# 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
  # @private
  class Tracer
    # @return [IO]
    attr_reader :io
    # @return [Integer]
    attr_accessor :tracing_level

    # @private
    # Classes to trace
    TRACED_CLASSES = [Types::Any, Types::Choice, Types::Sequence, Types::SequenceOf, Types::Base].freeze

    # @param io [IO] io to use to put traces
    def initialize(io)
      @io = io
      @tracing_level = 0
    end

    # Puts +msg+ onto {#io}.
    # @param msg [String]
    # @return [void]
    def trace(msg)
      @io.puts(indent << msg)
    end

    # Return identation for given +level+. If +nil+, use {#tracing_level}.
    # @param level [Integer,nil]
    # @return [String]
    def indent(level=nil)
      level ||= @tracing_level
      '  ' * level
    end
  end

  # Trace RASN1 parsing to +io+.
  # All parsing methods called in block are traced to +io+. Each ASN.1 element is
  # traced in a line showing element's id, its length and its data.
  # @param io [IO]
  # @return [void]
  # @example
  #   RASN1.trace do
  #     RASN1.parse("\x02\x01\x01")  # puts "INTEGER id: 2 (0x02), len: 1 (0x01), data: 0x01"
  #   end
  #   RASN1.parse("\x01\x01\xff")    # puts nothing onto STDOUT
  def self.trace(io=$stdout)
    @tracer = Tracer.new(io)
    Tracer::TRACED_CLASSES.each(&:start_tracing)

    begin
      yield @tracer
    ensure
      Tracer::TRACED_CLASSES.reverse.each(&:stop_tracing)
      @tracer.io.flush
      @tracer = nil
    end
  end

  # @private
  # @return [Tracer]
  def self.tracer
    @tracer
  end

  module Types
    class Base
      class << self
        # @private
        # Patch {#do_parse} to add tracing ability
        # @return [void]
        def start_tracing
          alias_method :do_parse_without_tracing, :do_parse
          alias_method :do_parse, :do_parse_with_tracing
          alias_method :do_parse_explicit_without_tracing, :do_parse_explicit
          alias_method :do_parse_explicit, :do_parse_explicit_with_tracing
        end

        # @private
        # Unpatch {#do_parse} to remove tracing ability
        # @return [void]
        def stop_tracing
          alias_method :do_parse, :do_parse_without_tracing
          alias_method :do_parse_explicit, :do_parse_explicit_without_tracing
        end
      end

      # @private
      # Parse +der+ with tracing abillity
      # @!macro do_parse
      # @see #parse!
      def do_parse_with_tracing(der, ber:)
        ret = do_parse_without_tracing(der, ber: ber)
        RASN1.tracer.trace(self.trace)
        ret
      end

      # @private
      # Delegate with tracing to explicit type to generate sub-value
      # @param data [String]
      # @return [void]
      # @see #parse!
      def do_parse_explicit_with_tracing(data)
        RASN1.tracer.tracing_level += 1
        do_parse_explicit_without_tracing(data)
        RASN1.tracer.tracing_level -= 1
      end
    end

    class Choice
      class << self
        # @private
        # Patch {#parse!} to add tracing ability
        # @return [void]
        def start_tracing
          alias_method :parse_without_tracing, :parse!
          alias_method :parse!, :parse_with_tracing
        end

        # @private
        # Unpatch {#parse!} to remove tracing ability
        # @return [void]
        def stop_tracing
          alias_method :parse!, :parse_without_tracing
        end
      end

      # @private
      # Parse +der+ with tracing abillity
      # @!macro do_parse
      # @see #parse!
      def parse_with_tracing(der, ber: false)
        RASN1.tracer.trace(self.trace)
        parse_without_tracing(der, ber: ber)
      end
    end

    class Sequence
      class << self
        # @private
        # Patch {#der_to_value} to add tracing ability
        # @return [void]
        def start_tracing
          alias_method :der_to_value_without_tracing, :der_to_value
          alias_method :der_to_value, :der_to_value_with_tracing
        end

        # @private
        # Unpatch {#der_to_value} to remove tracing ability
        # @return [void]
        def stop_tracing
          alias_method :der_to_value, :der_to_value_without_tracing
        end
      end

      # @private
      # der_to_value +der+ with tracing abillity
      # @!macro do_parse
      def der_to_value_with_tracing(der, ber: false)
        RASN1.tracer.tracing_level += 1
        der_to_value_without_tracing(der, ber: ber)
        RASN1.tracer.tracing_level -= 1
      end
    end

    class SequenceOf
      class << self
        # @private
        # Patch {#der_to_value} to add tracing ability
        # @return [void]
        def start_tracing
          alias_method :der_to_value_without_tracing, :der_to_value
          alias_method :der_to_value, :der_to_value_with_tracing
        end

        # @private
        # Unpatch {#der_to_value} to remove tracing ability
        # @return [void]
        def stop_tracing
          alias_method :der_to_value, :der_to_value_without_tracing
        end
      end

      # @private
      # der_to_value +der+ with tracing abillity
      # @!macro do_parse
      def der_to_value_with_tracing(der, ber: false)
        RASN1.tracer.tracing_level += 1
        der_to_value_without_tracing(der, ber: ber)
        RASN1.tracer.tracing_level -= 1
      end
    end
  end
end
