require 'rubyqrpay/version'
require_relative 'validator'
require_relative 'constants'
require 'rqrcode'
require 'digest/crc16_ccitt'
require 'base64'
require 'uri'

module Rubyqrpay
  class Generator
    def self.generate_payload(opts)
      opts = Rubyqrpay::Validator.validate_payload(opts)
      unless opts.nil?
        payload = generation(opts)
        # percent_encode payload # Temporary solution to fix invalid BNB URI decode
      end
    end

    def self.generate_png(url, payload, **opts)
      opts_default = {size: size_func(url, payload).to_i, level: :l}
      qrcode = RQRCode::QRCode.new("#{url}#{payload}", level: opts[:level] || opts_default[:level])
      png = qrcode.as_png(resize_gte_to: false,
                          resize_exactly_to: false,
                          fill: 'white',
                          color: 'black',
                          size: opts[:size] || opts_default[:size],
                          border_modules: 4,
                          module_px_size: 6)
      qrcode_to_base64(png)
    end

    private

    def self.size_func(url, payload)
      x = "#{url}#{payload}".size
      K_SIZE_FUNC * x + B_SIZE_FUNC # linear function (kx + b)
    end

    def self.qrcode_to_base64(png)
      base64 = Base64.encode64(png.to_s)
      base64.split("\n").join
    end

    def self.generation(opts)
      payload_data = {
        ID_PAYLOAD_FORMAT                => PAYLOAD_FORMAT_EMV_QRCPS_MERCHANT_PRESENTED_MODE,
        ID_POI_METHOD                    => opts[:amount] ? POI_METHOD_DYNAMIC : POI_METHOD_STATIC,
        ID_MERCHANT_INFORMATION_32       => merchant_account_32_data(opts[:merchant_account_32]),
        ID_MERCHANT_INFORMATION_33       => merchant_account_33_data(opts[:agregator_id], opts[:merchant_account_33]),
        ID_MERCHANT_CATEGORY_CODE        => mcc_format(opts[:merchant_category_code]),
        ID_TRANSACTION_CURRENCY          => opts[:currency],
        ID_TRANSACTION_AMOUNT            => format_amount(opts[:amount]),
        ID_TIP_OF_CONVENIENCE_INDICATOR  => format_indicator(opts[:convenience_indicator]),
        ID_COUNTRY                       => opts[:country],
        ID_MERCHANT_NAME                 => opts[:merchant_name],
        ID_MERCHANT_CITY                 => opts[:merchant_city],
        ID_POSTAL_CODE                   => opts[:postal_code],
        ID_ADDITIONAL_DATA_FIELD         => additional_data_field(opts[:additional_data]),
        ID_MERCHANT_INFORMATION_LANGUAGE => merchant_language_data(opts[:merchant_information_language])
      }
      payload_data = convenience_indicator_case(payload_data, opts)
      payload = join_hash(payload_data)
      payload += crc(payload)
    end

    def self.merchant_account_32_data(merchant_account)
      merchant_account = {
        MERCHANT_INFORMATION_TEMPLATE_ID_GUID => GUID_PROMPTPAY_32,
        ID_SERVICE_CODE_ERIP       => merchant_account[:service_code_erip],
        ID_PAYER_UNIQUE            => merchant_account[:payer_unique_id],
        ID_PAYER_NUMBER            => merchant_account[:payer_number],
        ID_AMOUNT_EDIT_POSSIBILITY => aep_convert(merchant_account[:amount_edit_possibility])
      }
      join_hash(merchant_account)
    end

    def self.aep_convert(aep)
      if aep || aep.nil?
        AEP_DEFAULT
      else
        AEP_FALSE
      end
    end

    def self.merchant_account_33_data(agregator_id, merchant_account)
      unless agregator_id.nil?
        merchant_account ||= Hash.new
        merchant_account = {
            MERCHANT_INFORMATION_TEMPLATE_ID_GUID => "#{GUID_PROMPTPAY_33}#{agregator_id}",
            ID_SERVICE_PRODUCER_CODE => merchant_account[:service_producer_code],
            ID_SERVICE_CODE          => merchant_account[:service_code],
            ID_OUTLET                => merchant_account[:outlet],
            ID_ORDER_CODE            => merchant_account[:order_code]
          }
        join_hash(merchant_account)
      end
    end

    def self.additional_data_field(additional_data)
      additional_data ||= Hash.new
      additional_data = {
        ID_BILL_NUMBER            => additional_data[:bill_number],
        ID_MOBILE_NUMBER          => additional_data[:mobile_number],
        ID_STORE_LABEL            => additional_data[:store_label],
        ID_LOYALTY_NUMBER         => additional_data[:loyalty_number],
        ID_REFERENCE_LABEL        => additional_data[:reference_label],
        ID_CUSTOMER_LABEL         => additional_data[:customer_label],
        ID_TERMINAL_LABEL         => additional_data[:terminal_label],
        ID_PURPOSE_OF_TRANSACTION => additional_data[:purpose_of_transaction],
        ID_CONSUMER_DATA_REQUEST  => additional_data[:consumer_data_request]
      }
      join_hash(additional_data)
    end

    def self.merchant_language_data(merchant_information_language)
      merchant_information_language ||= Hash.new
      merchant_information_language = {
        ID_LANGUAGE_REFERENCE      => merchant_information_language[:language_reference],
        ID_MERCHANT_NAME_ALTERNATE => merchant_information_language[:name_alternate],
        ID_MERCHANT_CITY_ALTERNATE => merchant_information_language[:city_alternate]
      }
      join_hash(merchant_information_language)
    end

    def self.convenience_indicator_case(payload_data, opts)
      payload_data.tap do |data|
        case format_indicator(opts[:convenience_indicator])
        when CONVENIENCE_INDICATOR_FIXED
          data[ID_VALUE_OF_CONVENIENCE_FEE_FIXED] = format_amount(opts[:fixed_fee])
        when CONVENIENCE_INDICATOR_PERCENTAGE
          data[ID_VALUE_OF_CONVENIENCE_FEE_PERCENTAGE] = format_amount(opts[:percentage_fee])
        end
      end
    end

    def self.join_hash(hash)
      hash.map do |id, value|
        value = percent_encode(value.to_s).gsub(/\%.{2}/, '')
        unless value.empty?
          len = "00#{value.size}".slice(-2..-1)
          id + len + value
        end
      end.join
    end

    def self.format_amount(amount)
      "%.2f" % amount.to_f
    end

    def self.format_indicator(convenience_indicator)
      "0#{convenience_indicator}"
    end

    def self.percent_encode(str)
      URI.escape(str)
    end

    def self.crc(data)
      # === old algorithm
      # data += ID_CRC + CRC_SYMBOL_SIZE
      # x = Digest::CRC16CCITT.new
      # x.update(data)
      # ID_CRC + CRC_SYMBOL_SIZE + x.hexdigest.upcase

      # === updated algorithm
      ID_CRC + CRC_SYMBOL_SIZE + Digest::SHA256.hexdigest(data).slice(-4..-1).upcase
    end

    def self.mcc_format(mcc)
      "%.4d" % mcc unless mcc.to_i == 0
    end
  end
end