# frozen_string_literal: true

require 'set'
require 'open3'
require 'rbconfig'

class CodeIdentity
  RULES = [
    ->(expression) { "(#{expression}).reverse.reverse" },
    ->(expression) { "(#{expression}).chars.join" }
  ].freeze

  def initialize(text, rules: RULES)
    unless text.is_a?(String) && text.encoding == Encoding::UTF_8 && text.valid_encoding?
      raise ArgumentError, 'text must be a valid UTF-8 string'
    end

    @literal = text.dump
    @rules = rules.dup.freeze
  end

  def original
    "print(#{@literal})"
  end

  def generate(limit = 20)
    unless limit.is_a?(Integer) && limit.positive?
      raise ArgumentError, 'limit must be a positive integer'
    end

    queue = [@literal]
    seen = Set.new(queue)
    programs = []
    cursor = 0

    while cursor < queue.length
      expression = queue[cursor]
      cursor += 1

      forms = [
        "print(#{expression})",
        "$stdout.write(#{expression})",
        "printf(\"%s\", #{expression})"
      ]
      forms.each do |source|
        programs << source
        return programs if programs.length == limit
      end

      @rules.each do |rule|
        candidate = rule.call(expression)
        queue << candidate if seen.add?(candidate)
      end
    end

    programs
  end

  def observe(source)
    stdout, stderr, status = Open3.capture3(
      RbConfig.ruby, '-EUTF-8:UTF-8', '-e', source, binmode: true
    )
    [stdout, stderr, status.exitstatus, status.termsig]
  end

  def verify!(programs)
    expected = observe(original)
    programs.each do |source|
      unless observe(source) == expected
        raise "Output mismatch for: #{source}"
      end
    end
    true
  end
end

if $PROGRAM_NAME == __FILE__
  require 'optparse'

  count = 20
  check = false
  parser = OptionParser.new do |options|
    options.banner = 'Usage: ruby code_identity.rb [options] [TEXT]'
    options.on('-n N', '--number N', Integer, 'Maximum number of programs') { |n| count = n }
    options.on('--check', 'Check stdout, stderr, and exit status') { check = true }
    options.on('-h', '--help', 'Show help') { puts options; exit }
  end

  begin
    parser.parse!
    raise ArgumentError, 'expected at most one TEXT argument' if ARGV.length > 1

    text = ARGV.empty? ? "Hello, World!\n" : ARGV.first
    generator = CodeIdentity.new(text)
    programs = generator.generate(count)
    generator.verify!(programs) if check
    puts programs
    warn "Verified #{programs.length} programs." if check
  rescue OptionParser::ParseError, ArgumentError => error
    warn error.message
    exit 1
  end
end
