# -*- encoding : utf-8 -*-
# frozen_string_literal: true
########################################################
## Thoughts from reading the ISO 32000-1:2008
## this file is part of the CombinePDF library and the code
## is subject to the same license.
########################################################

module CombinePDF
  #:nodoc: all

  protected

  # @private
  # @!visibility private

  # This is an internal class. you don't need it.
  class PDFDecrypt
    # include CombinePDF::Renderer

    # @!visibility private

    # make a new Decrypt object. requires:
    # objects:: an array containing the encrypted objects.
    # root_dictionary:: the root PDF dictionary, containing the Encrypt dictionary.
    def initialize(objects = [], root_dictionary = {})
      @objects = objects
      @encryption_dictionary = actual_object(root_dictionary[:Encrypt])
      raise EncryptionError, 'Cannot decrypt an encrypted file without an encryption dictionary!' unless @encryption_dictionary
      @root_dictionary = actual_object(root_dictionary)
      @padding_key = [0x28, 0xBF, 0x4E, 0x5E, 0x4E, 0x75, 0x8A, 0x41,
                      0x64, 0x00, 0x4E, 0x56, 0xFF, 0xFA, 0x01, 0x08,
                      0x2E, 0x2E, 0x00, 0xB6, 0xD0, 0x68, 0x3E, 0x80,
                      0x2F, 0x0C, 0xA9, 0xFE, 0x64, 0x53, 0x69, 0x7A]

      change_references_to_actual_values @encryption_dictionary
    end

    # call this to start the decryption.
    def decrypt
      raise_encrypted_error @encryption_dictionary unless @encryption_dictionary[:Filter] == :Standard
      @key = set_general_key
      case actual_object(@encryption_dictionary[:V])
      when 1, 2
        # raise_encrypted_error
        _perform_decrypt_proc_ @objects, method(:decrypt_RC4)
      when 4
        # make sure CF is a Hash (as required by the PDF standard for this type of encryption).
        raise_encrypted_error unless actual_object(@encryption_dictionary[:CF]).is_a?(Hash)

        # support trivial case for now
        # - same filter for streams (Stmf) and strings(Strf)
        # - AND :CFM == :V2 (use algorithm 1)
        raise_encrypted_error unless (@encryption_dictionary[:StmF] == @encryption_dictionary[:StrF])

        cfilter = actual_object(@encryption_dictionary[:CF])[@encryption_dictionary[:StrF]]
        raise_encrypted_error unless cfilter
        raise_encrypted_error unless (cfilter[:AuthEvent] == :DocOpen)
        if (cfilter[:CFM] == :V2)
          _perform_decrypt_proc_ @objects, method(:decrypt_RC4)
        elsif (cfilter[:CFM] == :AESV2)
          _perform_decrypt_proc_ @objects, method(:decrypt_AES)
        else
          raise_encrypted_error
        end
      end
      # rebuild stream lengths?
      @objects
    rescue => e
      puts e
      puts e.message
      puts e.backtrace.join("\n")
      raise_encrypted_error
    end

    protected

    def set_general_key(password = '')
      # 1) make sure the initial key is 32 byte long (if no password, uses padding).
      key = (password.bytes[0..32].to_a + @padding_key)[0..31].to_a.pack('C*').force_encoding(Encoding::ASCII_8BIT)
      # 2) add the value of the encryption dictionary’s O entry
      key << actual_object(@encryption_dictionary[:O]).to_s
      # 3) Convert the integer value of the P entry to a 32-bit unsigned binary number
      # and pass these bytes low-order byte first
      key << [actual_object(@encryption_dictionary[:P])].pack('i')
      # 4) Pass the first element of the file’s file identifier array
      # (the value of the ID entry in the document’s trailer dictionary
      key << actual_object(@root_dictionary[:ID])[0]
      # # 4(a) (Security handlers of revision 4 or greater)
      # # if document metadata is not being encrypted, add 4 bytes with the value 0xFFFFFFFF.
      if actual_object(@encryption_dictionary[:R]) >= 4
        if actual_object(@encryption_dictionary)[:EncryptMetadata] == false
          key << "\xFF\xFF\xFF\xFF".force_encoding(Encoding::ASCII_8BIT)
        end
      end
      # 5) pass everything as a MD5 hash
      key = Digest::MD5.digest(key)
      # 5(a) h) (Security handlers of revision 3 or greater) Do the following 50 times:
      # Take the output from the previous MD5 hash and
      # pass the first n bytes of the output as input into a new MD5 hash,
      # where n is the number of bytes of the encryption key as defined by the value of
      # the encryption dictionary’s Length entry.
      if actual_object(@encryption_dictionary[:R]) >= 3
        50.times do |_i|
          key = Digest::MD5.digest(key[0...actual_object(@encryption_dictionary[:Length])])
        end
      end
      # 6) Set the encryption key to the first n bytes of the output from the final MD5 hash,
      # where n shall always be 5 for security handlers of revision 2 but,
      # for security handlers of revision 3 or greater,
      # shall depend on the value of the encryption dictionary’s Length entry.
      @key = if actual_object(@encryption_dictionary[:R]) >= 3
               key[0..(actual_object(@encryption_dictionary[:Length]) / 8)]
             else
               key[0..4]
             end
      @key
    end

    def decrypt_none(_encrypted, _encrypted_id, _encrypted_generation, _encrypted_filter)
      'encrypted'
    end

    def decrypt_RC4(encrypted, encrypted_id, encrypted_generation, _encrypted_filter)
      ## start decryption using padding strings
      object_key = @key.dup
      object_key << [encrypted_id].pack('i')[0..2]
      object_key << [encrypted_generation].pack('i')[0..1]
      # (0..2).each { |e| object_key << (encrypted_id >> e*8 & 0xFF ) }
      # (0..1).each { |e| object_key << (encrypted_generation >> e*8 & 0xFF ) }
      key_length = object_key.length < 16 ? object_key.length : 16
      rc4 = ::RC4.new(Digest::MD5.digest(object_key)[(0...key_length)])
      rc4.decrypt(encrypted)
    end

    def decrypt_AES(encrypted, encrypted_id, encrypted_generation, _encrypted_filter)
      ## start decryption using padding strings
      object_key = @key.dup
      object_key << [encrypted_id].pack('i')[0..2]
      object_key << [encrypted_generation].pack('i')[0..1]
      object_key << 'sAlT'.b
      key_length = object_key.length < 16 ? object_key.length : 16

      begin
        cipher = OpenSSL::Cipher.new("aes-#{key_length << 3}-cbc")
        cipher.decrypt
        cipher.key = Digest::MD5.digest(object_key)[(0...key_length)]
        cipher.iv = encrypted[0..15]
        cipher.padding = 0
        cipher.update(encrypted[16..-1]) + cipher.final
      rescue StandardError => _e
        # puts e.class.name
        encrypted
      end
    end

    protected

    def _perform_decrypt_proc_(object, decrypt_proc, encrypted_id = nil, encrypted_generation = nil, encrypted_filter = nil)
      if object.is_a?(Array)
        object.map! { |item| _perform_decrypt_proc_(item, decrypt_proc, encrypted_id, encrypted_generation, encrypted_filter) }
      elsif object.is_a?(Hash)
        encrypted_id ||= actual_object(object[:indirect_reference_id])
        encrypted_generation ||= actual_object(object[:indirect_generation_number])
        encrypted_filter ||= actual_object(object[:Filter])
        if object[:raw_stream_content] && !object[:raw_stream_content].empty?
          stream_length = actual_object(object[:Length])
          actual_length = object[:raw_stream_content].bytesize
          # p stream_length
          # p actual_length
          # p object[:Length]
          # p object
          warn "Stream registered length was #{object[:Length]} and the actual length was #{actual_length}." if actual_length < stream_length
          length = [stream_length, actual_length].min
          object[:raw_stream_content] = decrypt_proc.call((object[:raw_stream_content][0...length]), encrypted_id, encrypted_generation, encrypted_filter)
        end
        object.each { |k, v| object[k] = _perform_decrypt_proc_(v, decrypt_proc, encrypted_id, encrypted_generation, encrypted_filter) if k != :raw_stream_content && (v.is_a?(Hash) || v.is_a?(Array) || v.is_a?(String)) } # assumes no decrypting is never performed on keys
      elsif object.is_a?(String)
        return decrypt_proc.call(object, encrypted_id, encrypted_generation, encrypted_filter)
      else
        return object
      end
    end

    def raise_encrypted_error(object = nil)
      object ||= @encryption_dictionary.to_s.split(',').join("\n")
      warn "Data raising exception:\n #{object.to_s.split(',').join("\n")}"
      raise EncryptionError, 'File is encrypted - not supported.'
    end

    def change_references_to_actual_values(hash_with_references = {})
      hash_with_references.each do |k, v|
        next unless v.is_a?(Hash) && v[:is_reference_only]
        hash_with_references[k] = get_refernced_object(v)
        hash_with_references[k] = hash_with_references[k][:indirect_without_dictionary] if hash_with_references[k].is_a?(Hash) && hash_with_references[k][:indirect_without_dictionary]
        warn "Couldn't connect all values from references - didn't find reference #{hash_with_references}!!!" if hash_with_references[k].nil?
        hash_with_references[k] = v unless hash_with_references[k]
      end
      hash_with_references
    end

    def get_refernced_object(reference_hash = {})
      @objects.each do |stored_object|
        return (stored_object[:indirect_without_dictionary] || stored_object) if stored_object.is_a?(Hash) &&
                                                                                 reference_hash[:indirect_reference_id] == stored_object[:indirect_reference_id] &&
                                                                                 reference_hash[:indirect_generation_number] == stored_object[:indirect_generation_number]
      end
      warn "didn't find reference #{reference_hash}"
      nil
    end

    def actual_object(obj)
      return get_refernced_object(obj) if obj.is_a?(Hash) && obj[:indirect_reference_id]
      obj
    end

    # # returns the PDF Object Hash holding the acutal data (if exists) or the original hash (if it wasn't a reference)
    # #
    # # works only AFTER references have been connected.
    # def get_referenced object
    # 	object[:referenced_object] || object
    # end
  end
  #####################################################
  ## The following isn't my code!!!!
  ## It is subject to a different license and copyright.
  ## This was the code for the RC4 Gem,
  ## ... I had a bad internet connection so I ended up
  ## copying it from the web page I had in my cache.
  ## This wonderful work was done by Caige Nichols.
  #####################################################
  # class RC4
  #   def initialize(str)
  #     begin
  #       raise SyntaxError, "RC4: Key supplied is blank"  if str.eql?('')

  #       @q1, @q2 = 0, 0
  #       @key = []
  #       str.each_byte { |elem| @key << elem } while @key.size < 256
  #       @key.slice!(256..@key.size-1) if @key.size >= 256
  #       @s = (0..255).to_a
  #       j = 0
  #       0.upto(255) do |i|
  #         j = (j + @s[i] + @key[i] ) % 256
  #         @s[i], @s[j] = @s[j], @s[i]
  #       end
  #     end
  #   end

  #   def encrypt!(text)
  #     process text
  #   end

  #   def encrypt(text)
  #     process text.dup
  #   end

  #   alias_method :decrypt, :encrypt

  #   private

  #   def process(text)
  #     text.unpack("C*").map { |c| c ^ round }.pack("C*")
  #   end

  #   def round
  #     @q1 = (@q1 + 1) % 256
  #     @q2 = (@q2 + @s[@q1]) % 256
  #     @s[@q1], @s[@q2] = @s[@q2], @s[@q1]
  #     @s[(@s[@q1]+@s[@q2]) % 256]
  #   end
  # end
end
