#!/usr/bin/ruby

require 'json'
require 'ostruct'
require 'digest'

def calculate_md5sum( filename )
    Digest::MD5.file( filename ).hexdigest
end

def output( code = '' )
    $output ||= ""
    $indentation = 0 if $indentation == nil
    lines = code.strip.split( "\n" ).map do |line|
        balanced_braces = line.count( "{" ) == line.count( "}" )

        if balanced_braces == false && (line.strip.end_with?( '}' ) || line.strip.end_with?( '];' ) || line.strip.end_with?( '},' ))
            $indentation -= 4
        elsif line.strip == ')'
            $indentation -= 4
        end

        line = (' ' * $indentation) + line.strip
        if line.end_with?( '{' ) || line.end_with?( '[' ) || line.end_with?( '(' )
            $indentation += 4
        end

        line
    end

    $output << lines.join( "\n" )
    $output << "\n"
end

def write_output_to_file( filename )
    STDERR.puts "Generating #{filename}..."
    File.open( get_path( filename ), "wb" ) do |fp|
        fp.puts $output
    end
end

def get_path( filename )
    File.expand_path File.join( File.dirname( __FILE__ ), filename )
end

def get_files( directory, extension )
    root_path = get_path( ".." );
    absolute_path = get_path( directory )
    Dir.glob( "#{absolute_path}/**/*.#{extension}" ).map do |path|
        OpenStruct.new({
            relative_path_from_here: ".." + path[ root_path.length..-1 ],
            relative_path: path[ absolute_path.length + 1..-1 ],
            absolute_path: path
        })
    end.sort { |a, b| a.absolute_path <=> b.absolute_path }
end

def sanitize_name( name )
    name.downcase.gsub /[^a-zA-Z0-9]/, "_"
end

def load_roms
    rom_path_by_md5sum = {}
    rom_path_list = get_files( "../roms", "nes" )
    rom_path_list.each do |path|
        md5sum = calculate_md5sum path.absolute_path
        rom_path_by_md5sum[ md5sum ] = path
    end

    rom_path_by_md5sum
end

def load_tests( rom_path_by_md5sum )
    test_path_list = get_files( "../testcases", "json" )
    test_tree = {}

    test_path_list.each do |path|
        test = JSON.parse File.read( path.absolute_path ), { symbolize_names: true }
        relative_path_chunks = path.relative_path.split "/"

        test_name = sanitize_name( File.basename relative_path_chunks[ -1 ], ".json" )
        path_chunks = relative_path_chunks[ 0..-2 ].map { |chunk| sanitize_name chunk }

        obj = test_tree
        path_chunks.each do |chunk|
            obj[ :nested ] ||= {}
            obj[ :tests ] ||= {}

            obj[ :nested ][ chunk ] ||= {}
            obj = obj[ :nested ][ chunk ]
        end

        obj[ :nested ] ||= {}
        obj[ :tests ] ||= {}
        obj[ :tests ][ test_name ] = test

        md5sum = test[ :romfile_md5sum ]
        unless rom_path_by_md5sum.has_key? md5sum
            STDERR.puts "#{path.absolute_path}: ROM with an md5sum of #{md5sum} not found!"
            exit 1
        end

        test[ :rom_path ] = rom_path_by_md5sum[ md5sum ]
        test[ :rom_path ].used = true
    end
    test_tree
end

def generate_test_code_for_node name, test_tree_node, depth = 1
    output "pub mod #{name} {" if name

    test_tree_node[ :nested ].each do |subtest_name, subtest_node|
        generate_test_code_for_node subtest_name, subtest_node, depth + 1
    end

    generated_use_harness = false
    test_tree_node[ :tests ].each do |test_name, test|
        rom_path = test[ :rom_path ].relative_path_from_here

        unless generated_use_harness
            output "use harness;"
            generated_use_harness = true
        end

        output "pub fn testcase_#{test_name}< T: harness::EmulatorInterface >() {"
        output "static ROM: &'static [u8] = include_bytes!( \"#{rom_path}\" );"
        output ""

        output "harness::standard_testcase::< T >( ROM, \"#{test[ :test ][ :expected_framebuffer_md5sum ]}\", #{test[ :test ][ :elapsed_frames ]} );"
        output "}"
    end

    output "}" if name
end

def generate_testsuite_for_node name, test_tree_node, path = []
    if name
        path << name
        output "mod #{name} {"
    end

    test_tree_node[ :nested ].each do |subtest_name, subtest_node|
        generate_testsuite_for_node subtest_name, subtest_node, path
    end

    test_tree_node[ :tests ].each do |test_name, test|
        output "#[test]"
        output "fn testcase_#{test_name}() {"
        output "use #{ (([ "super" ] * path.length) + [ "$interface" ]).join "::" };"
        output "#{ ([ "$crate", "tests" ] + path + [ "testcase_" + test_name, "< $interface >" ]).join "::" }();"
        output "}"
    end

    if name
        output "}"
        path.pop
    end
end

rom_path_by_md5sum = load_roms
test_tree = load_tests rom_path_by_md5sum

output "// WARNING: This file was autogenerated; DO NOT EDIT."
output
output "#[doc(hidden)]"
generate_test_code_for_node nil, test_tree
output

output "#[macro_export]"
output "macro_rules! nes_testsuite {"
output "($interface:ident) => ("
generate_testsuite_for_node nil, test_tree
output ")"
output "}"

unused_roms = rom_path_by_md5sum.values.select { |rom_path| !rom_path.used }
unless unused_roms.empty?
    STDERR.puts "Warning: the following ROMs are unused:"
    unused_roms.each do |path|
        STDERR.puts "    #{path.relative_path}"
    end
end

write_output_to_file "tests.rs"
