#!/usr/bin/perl

use strict;
use integer;

my @FW_MAGIC = (undef, 0x7a07fbd6, 0xa924ed0b);

my %datatypes = (
    'end'       => 0,		# End of data
    'data'      => 1,		# FPGA flash data
    'target'    => 2,		# Firmware target mask and string
    'note'      => 3,		# Informative string
    'espota'    => 4,		# ESP32 OTA image
    'fpgainit'  => 5,		# FPGA bypass (transient) image during update
    'esppart'   => 6,		# ESP32 partition table
    'espsys'    => 7,		# ESP32 boot loader, OTA control partition...
    'esptool'   => 8,		# esptool.py options for flashing
    'boardinfo' => 9		# board_info block address (FPGA)
    );
my @type;
foreach my $t (keys(%datatypes)) {
    $type[$datatypes{$t}] = $t;
}

my $FDF_OPTIONAL = 0x0001;

my $STRING_MAX_LEN = 4095;

my %int_shifts = ('' => 0, 'k' => 10, 'm' => 20, 'g' => 30,
		  't' => 40, 'p' => 50, 'e' => 60);
sub getint($) {
    my($s) = @_;

    return undef
	unless ($s =~ /^(([1-9][0-9]*)|(0(x[0-9a-f]+|[0-7]*)))([kmgtpe]?)$/i);

    return (oct($3) + $2) << $int_shifts{lc($5)};
}
sub filelen($) {
    my($f) = @_;
    my @s = stat($f);

    return $s[7];
}

our $outfile;

my %default_options = (
    'fw'      => 1,		# Default and minimum firmware version
    'type'    => $datatypes{'data'},
    'addr'    => 0,
    'flags'   => 0,
    'vmatch'  => 0,
    'vmask'   => 0,
    'vmin'    => 0,
    'vmax'    => 0xffff
);
my %need_versions = (
    'vmatch' => 2, 'vmask' => 2, 'vmin' => 2, 'vmax' => 2
);

sub output_chunk($$%) {
    my($out,$data,%opts) = @_;

    foreach my $o (keys(%default_options)) {
	$opts{$o} = $default_options{$o} unless (defined($opts{$o}));
    }

    $opts{'vmatch'} &= 0xffffffff;
    $opts{'vmask'}  &= 0xffffffff;
    $opts{'vmin'}   &= 0xffff;
    $opts{'vmax'}   &= 0xffff;

    my $version = $opts{'fw'};
    foreach my $o (keys(%need_versions)) {
	if ($opts{$o} ne $default_options{$o} &&
	    $need_versions{$o} > $version) {
		$version = $need_versions{$o};
	}
    }

    if (!defined($FW_MAGIC[$version])) {
	die "$0: $outfile: invalid firmware format version: $version\n";
    }

    print $out pack('VvvVV',
		    $FW_MAGIC[$version],
		    $opts{'type'}, $opts{'flags'},
		    length($data), $opts{'addr'});
    if ($version >= 2) {
	print $out pack('VVvvV',
			$opts{'vmatch'}, $opts{'vmask'},
			$opts{'vmin'}, $opts{'vmax'},
			0);
    }
    printf STDERR "chunk: fw %u type %s (%u) flags 0x%x length %u addr 0x%x ver 0x%x/0x%x,%u:%u\n",
	$version, $type[$opts{'type'}], $opts{'type'},
	$opts{'flags'},
	length($data), $opts{'addr'},
	$opts{'vmatch'}, $opts{'vmask'}, $opts{'vmin'}, $opts{'vmax'};
    print $out $data;
}

if (!scalar(@ARGV)) {
    die "Usage: $0 [-o outfile] [-fwmin ver] [options command]...\n".
	"Options:\n".
	"\t-type datatype\n".
	"\t-addr address (or equivalent)\n".
	"\t-optional\n".
	"\t-required\n".
	"\t-ver match,mask,min,max\n".
	"Commands:\n".
	"\t-file data_file\n".
	"\t-str data_string\n";
}

our $out;

sub delete_out {
    close($out);
    unlink($outfile);
}

if ($ARGV[0] eq '-o') {
    shift @ARGV;
    $outfile = shift @ARGV;
}
if ($outfile ne '' && $outfile ne '-') {
    open($out, '>', $outfile) or
	die "$0: $outfile: $!\n";

    $SIG{'INT'}     = \&delete_out;
    $SIG{'QUIT'}    = \&delete_out;
    $SIG{'TERM'}    = \&delete_out;
    $SIG{'__DIE__'} = \&delete_out;
} else {
    $outfile = '-';
    $out = \*STDOUT;
}

binmode $out;

my $err;
my %options = ();
while (1) {
    my $what = shift @ARGV;

    last if (!defined($what));

    if ($what eq '-type') {
	my $arg = lc(shift @ARGV);
	my $type = defined($datatypes{$arg}) ? $datatypes{$arg} : getint($arg);
	if (!defined($type)) {
	    die "$0: invalid data type: $arg";
	}
	$options{'type'} = $type;
    } elsif ($what =~ /^\-(addr|flags|vmatch|vmask|vmin|vmax)$/) {
	my $opt = $1;
	my $arg = getint(shift @ARGV);
	$options{$opt} = $arg if (defined($arg));
    } elsif ($what eq '-fwmin') {
	my $arg = getint(shift @ARGV);
	$default_options{'fw'} = $arg if (defined($arg));
    } elsif ($what eq '-ver') {
	my @vp = split(/(?:[,:\/]|\.+)/, shift @ARGV);
	foreach my $opt (qw(vmatch vmask vmin vmax)) {
	    my $arg = getint(shift @vp);
	    $options{$opt} = $arg if (defined($arg));
	}
    } elsif ($what eq '-optional') {
	$options{'flags'} |= $FDF_OPTIONAL;
    } elsif ($what eq '-required') {
	$options{'flags'} &= ~$FDF_OPTIONAL;
    } elsif ($what eq '-file') {
	my $infile = shift @ARGV;

	my $in;
	if (!open($in, '<', $infile)) {
	    die "$0: $infile: $!\n";
	}
	binmode($in);
	my @is = stat($in);
	my $data;
	my $dlen = read($in, $data, $is[7]);
	close($in);

	output_chunk($out, $data, %options);
	%options = ();
    } elsif ($what eq '-str') {
	my $str = shift @ARGV;

	if (length($str) > $STRING_MAX_LEN) {
	    die "$0: $outfile: string too long\n";
	}
	
	output_chunk($out, $str, %options);
	%options = ();
    } elsif ($what eq '-empty') {
	output_chunk($out, '', %options);
	%options = ();
    } else {
	die "$0: unknown argument: $what\n";
    }
}

output_chunk($out, '', ('type' => $datatypes{'end'}));
close($out);
