#!/usr/bin/perl -w
# SPDX-License-Identifier: GPL-2.0-only OR BSD-3-Clause
# Copyright (C) 2019--2020 Intel Corporation

use Getopt::Long qw(:config no_ignore_case);
use File::Basename;

my $ccsregs = "ccs-regs.asc";
my $header;
my $regarray;
my $limitc;
my $limith;
my $kernel;
my $help;

GetOptions("ccsregs|c=s" => \$ccsregs,
	   "header|e=s" => \$header,
	   "regarray|r=s" => \$regarray,
	   "limitc|l=s" => \$limitc,
	   "limith|L=s" => \$limith,
	   "kernel|k" => \$kernel,
	   "help|h" => \$help) or die "can't parse options";

$help = 1 if ! defined $header || ! defined $limitc || ! defined $limith;

if (defined $help) {
	print <<EOH
$0 - Create CCS register definitions for C

usage: $0 -c ccs-regs.asc -e header -r regarray -l limit-c -L limit-header [-k]

	-c ccs register file
	-e header file name
	-r register description array file name
	-l limit and capability array file name
	-L limit and capability header file name
	-k generate files for kernel space consumption
EOH
	  ;
	exit 0;
}

my $lh_hdr = ! defined $kernel
	? '#include "ccs-os.h"' . "\n"
	: "#include <linux/bits.h>\n#include <linux/types.h>\n";
my $uint32_t = ! defined $kernel ? 'uint32_t' : 'u32';
my $uint16_t = ! defined $kernel ? 'uint16_t' : 'u16';

open(my $R, "< $ccsregs") or die "can't open $ccsregs";

open(my $H, "> $header") or die "can't open $header";
my $A;
if (defined $regarray) {
	open($A, "> $regarray") or die "can't open $regarray";
}
open(my $LC, "> $limitc") or die "can't open $limitc";
open(my $LH, "> $limith") or die "can't open $limith";

my %this;

sub is_limit_reg($) {
	my $addr = hex $_[0];

	return 0 if $addr < 0x40; # weed out status registers
	return 0 if $addr >= 0x100 && $addr < 0xfff; # weed out configuration registers

	return 1;
}

my $uc_header = basename uc $header;
$uc_header =~ s/[^A-Z0-9]/_/g;

my $copyright = "/* Copyright (C) 2019--2020 Intel Corporation */\n";
my $license = "SPDX-License-Identifier: GPL-2.0-only OR BSD-3-Clause";

for my $fh ($A, $LC) {
	print $fh "// $license\n$copyright\n" if defined $fh;
}

for my $fh ($H, $LH) {
	print $fh "/* $license */\n$copyright\n";
}

sub bit_def($) {
	my $bit = shift @_;

	return "BIT($bit)" if defined $kernel;
	return "(1U << $bit)" if $bit =~ /^[a-zA-Z0-9_]+$/;
	return "(1U << ($bit))";
}

print $H <<EOF
#ifndef __${uc_header}__
#define __${uc_header}__

EOF
  ;

print $H "#include <linux/bits.h>\n\n" if defined $kernel;

print $H <<EOF
#define CCS_FL_BASE		16
EOF
  ;

print $H "#define CCS_FL_16BIT		" . bit_def("CCS_FL_BASE") . "\n";
print $H "#define CCS_FL_32BIT		" . bit_def("CCS_FL_BASE + 1") . "\n";
print $H "#define CCS_FL_FLOAT_IREAL	" . bit_def("CCS_FL_BASE + 2") . "\n";
print $H "#define CCS_FL_IREAL		" . bit_def("CCS_FL_BASE + 3") . "\n";

print $H <<EOF
#define CCS_R_ADDR(r)		((r) & 0xffff)

EOF
  ;

print $A <<EOF
#include <stdint.h>
#include <stdio.h>
#include "ccs-extra.h"
#include "ccs-regs.h"

EOF
	if defined $A;

my $uc_limith = basename uc $limith;
$uc_limith =~ s/[^A-Z0-9]/_/g;

print $LH <<EOF
#ifndef __${uc_limith}__
#define __${uc_limith}__

$lh_hdr
struct ccs_limit {
	$uint32_t reg;
	$uint16_t size;
	$uint16_t flags;
	const char *name;
};

EOF
  ;
print $LH "#define CCS_L_FL_SAME_REG	" . bit_def(0) . "\n\n";

print $LH <<EOF
extern const struct ccs_limit ccs_limits[];

EOF
  ;

print $LC <<EOF
#include "ccs-limits.h"
#include "ccs-regs.h"

const struct ccs_limit ccs_limits[] = {
EOF
  ;

my $limitcount = 0;
my $argdescs;
my $reglist = "const struct ccs_reg_desc ccs_reg_desc[] = {\n";

sub name_split($$) {
	my ($name, $addr) = @_;
	my $args;

	$name =~ /([^\(]+?)(\(.*)/;
	($name, $args) = ($1, $2);
	$args = [split /,\s*/, $args];
	foreach my $t (@$args) {
		$t =~ s/[\(\)]//g;
		$t =~ s/\//\\\//g;
	}

	return ($name, $addr, $args);
}

sub tabconv($) {
	$_ = shift;

	my @l = split "\n", $_;

	map {
		s/ {8,8}/\t/g;
		s/\t\K +//;
	} @l;

	return (join "\n", @l) . "\n";
}

sub elem_size(@) {
	my @flags = @_;

	return 2 if grep /^16$/, @flags;
	return 4 if grep /^32$/, @flags;
	return 1;
}

sub arr_size($) {
	my $this = $_[0];
	my $size = $this->{elsize};
	my $h = $this->{argparams};

	foreach my $arg (@{$this->{args}}) {
		my $apref = $h->{$arg};

		$size *= $apref->{max} - $apref->{min} + 1;
	}

	return $size;
}

sub print_args($$$) {
	my ($this, $postfix, $is_same_reg) = @_;
	my ($args, $argparams, $name) =
	  ($this->{args}, $this->{argparams}, $this->{name});
	my $varname = "ccs_reg_arg_" . (lc $name) . $postfix;
	my @mins;
	my @sorted_args = @{$this->{sorted_args}};
	my $lim_arg;
	my $size = arr_size($this);

	$argdescs .= "static const struct ccs_reg_arg " . $varname . "[] = {\n";

	foreach my $sorted_arg (@sorted_args) {
		push @mins, $argparams->{$sorted_arg}->{min};
	}

	foreach my $sorted_arg (@sorted_args) {
		my $h = $argparams->{$sorted_arg};

		$argdescs .= "\t{ \"$sorted_arg\", $h->{min}, $h->{max}, $h->{elsize} },\n";

		$lim_arg .= defined $lim_arg ? ", $h->{min}" : "$h->{min}";
	}

	$argdescs .= "};\n\n";

	$reglist .= "\t{ CCS_R_" . (uc $name) . "(" . (join ",", (@mins)) .
	  "), $size, sizeof($varname) / sizeof(*$varname)," .
	    " \"" . (lc $name) . "\", $varname },\n";

	print $LC tabconv sprintf "\t{ CCS_R_" . (uc $name) . "($lim_arg), " .
	  $size . ", " . ($is_same_reg ? "CCS_L_FL_SAME_REG" : "0") .
	    ", \"$name" . (defined $this->{discontig} ? " $lim_arg" : "") . "\" },\n"
	      if is_limit_reg $this->{base_addr};
}

my $hdr_data;

while (<$R>) {
	chop;
	s/^\s*//;
	next if /^[#;]/ || /^$/;
	if (s/^-\s*//) {
		if (s/^b\s*//) {
			my ($bit, $addr) = split /\t+/;
			$bit = uc $bit;
			$hdr_data .= sprintf "#define %-62s %s", "CCS_" . (uc ${this{name}}) ."_$bit", bit_def($addr) . "\n";
		} elsif (s/^f\s*//) {
			s/[,\.-]/_/g;
			my @a = split /\s+/;
			my ($msb, $lsb, $this_field) = reverse @a;
		        @a = ( { "name" => "SHIFT", "addr" => $lsb, "fmt" => "%uU", },
			       { "name" => "MASK", "addr" => (1 << ($msb + 1)) - 1 - ((1 << $lsb) - 1), "fmt" => "0x%" . join(".", ($this{"elsize"} >> 2) x 2) . "x" } );
			$this{"field"} = $this_field;
			foreach my $ar (@a) {
				#print $ar->{fmt}."\n";
				$hdr_data .= sprintf "#define %-62s " . $ar->{"fmt"} . "\n", "CCS_" . (uc $this{"name"}) . (defined $this_field ? "_" . uc $this_field : "") . "_" . $ar->{"name"}, $ar->{"addr"} . "\n";
			}
		} elsif (s/^e\s*//) {
			s/[,\.-]/_/g;
			my ($enum, $addr) = split /\s+/;
			$enum = uc $enum;
			$hdr_data .= sprintf "#define %-62s %s", "CCS_" . (uc ${this{name}}) . (defined $this{"field"} ? "_" . uc $this{"field"} : "") ."_$enum", $addr . ($addr =~ /0x/i ? "" : "U") . "\n";
		} elsif (s/^l\s*//) {
			my ($arg, $min, $max, $elsize, @discontig) = split /\s+/;
			my $size;

			foreach my $num ($min, $max) {
				$num = hex $num if $num =~ /0x/i;
			}

			$hdr_data .= sprintf "#define %-62s %s", "CCS_LIM_" . (uc ${this{name}} . "_MIN_$arg"), $min . ($min =~ /0x/i ? "" : "U") . "\n";
			$hdr_data .= sprintf "#define %-62s %s", "CCS_LIM_" . (uc ${this{name}} . "_MAX_$arg"), $max . ($max =~ /0x/i ? "" : "U") . "\n";

			my $h = $this{argparams};

			$h->{$arg} = { "min" => $min,
				       "max" => $max,
				       "elsize" => $elsize =~ /^0x/ ? hex $elsize : $elsize,
				       "discontig" => \@discontig };

			$this{discontig} = $arg if @discontig;

			next if $#{$this{args}} + 1 != scalar keys %{$this{argparams}};

			my $reg_formula = "($this{addr}";
			my $lim_formula;

			foreach my $arg (@{$this{args}}) {
				my $d = $h->{$arg}->{discontig};
				my $times = $h->{$arg}->{elsize} != 1 ?
				  " * " . $h->{$arg}->{elsize} : "";

				if (@$d) {
					my ($lim, $offset) = split /,/, $d->[0];

					$reg_formula .= " + (($arg) < $lim ? ($arg)$times : $offset + (($arg) - $lim)$times)";
				} else {
					$reg_formula .= " + ($arg)$times";
				}

				$lim_formula .= (defined $lim_formula ? " + " : "") . "($arg)$times";
			}

			$reg_formula .= ")\n";
			$lim_formula =~ s/^\(([a-z0-9]+)\)$/$1/i;

			print $H tabconv sprintf("#define %-62s %s", "CCS_R_" . (uc $this{name}) .
			  $this{arglist}, $reg_formula);

			print $H tabconv $hdr_data;
			undef $hdr_data;

			# Sort arguments in descending order by size
			@{$this{sorted_args}} = sort {
				$h->{$a}->{elsize} <= $h->{$b}->{elsize}
			} @{$this{args}};

			if (defined $this{discontig}) {
				my $da = $this{argparams}->{$this{discontig}};
				my ($first_discontig) = split /,/, $da->{discontig}->[0];
				my $max = $da->{max};

				$da->{max} = $first_discontig - 1;
				print_args(\%this, "", 0);

				$da->{min} = $da->{max} + 1;
				$da->{max} = $max;
				print_args(\%this, $first_discontig, 1);
			} else {
				print_args(\%this, "", 0);
			}

			next unless is_limit_reg $this{base_addr};

			print $LH tabconv sprintf "#define %-63s%s\n",
			  "CCS_L_" . (uc $this{name}) . "_OFFSET(" .
			    (join ", ", @{$this{args}}) . ")", "($lim_formula)";
		}

		if (! @{$this{args}}) {
			print $H tabconv($hdr_data);
			undef $hdr_data;
		}

		next;
	}

	my ($name, $addr, @flags) = split /\t+/, $_;
	my $args = [];

	my $sp;

	($name, $addr, $args) = name_split($name, $addr) if /\(.*\)/;

	$name =~ s/[,\.-]/_/g;

	my $flagstring = "";
	my $size = elem_size(@flags);
	$flagstring .= "| CCS_FL_16BIT " if $size eq "2";
	$flagstring .= "| CCS_FL_32BIT " if $size eq "4";
	$flagstring .= "| CCS_FL_FLOAT_IREAL " if grep /^float_ireal$/, @flags;
	$flagstring .= "| CCS_FL_IREAL " if grep /^ireal$/, @flags;
	$flagstring =~ s/^\| //;
	$flagstring =~ s/ $//;
	$flagstring = "($flagstring)" if $flagstring =~ /\|/;
	my $base_addr = $addr;
	$addr = "($addr | $flagstring)" if $flagstring ne "";

	my $arglist = @$args ? "(" . (join ", ", @$args) . ")" : "";
	$hdr_data .= sprintf "#define %-62s %s\n", "CCS_R_" . (uc $name), $addr
	  if !@$args;

	$name =~ s/\(.*//;

	%this = ( name => $name,
		  addr => $addr,
		  base_addr => $base_addr,
		  argparams => {},
		  args => $args,
		  arglist => $arglist,
		  elsize => $size,
		);

	if (!@$args) {
		$reglist .= "\t{ CCS_R_" . (uc $name) . ", 1,  0, \"" . (lc $name) . "\", NULL },\n";
		print $H tabconv $hdr_data;
		undef $hdr_data;

		print $LC tabconv sprintf "\t{ CCS_R_" . (uc $name) . ", " .
		  $this{elsize} . ", 0, \"$name\" },\n"
		    if is_limit_reg $this{base_addr};
	}

	print $LH tabconv sprintf "#define %-63s%s\n",
	  "CCS_L_" . (uc $this{name}), $limitcount++
	    if is_limit_reg $this{base_addr};
}

if (defined $A) {
	print $A $argdescs, $reglist;

	print $A "\t{ 0 }\n";

	print $A "};\n";
}

print $H "\n#endif /* __${uc_header}__ */\n";

print $LH tabconv sprintf "#define %-63s%s\n", "CCS_L_LAST", $limitcount;

print $LH "\n#endif /* __${uc_limith}__ */\n";

print $LC "\t{ 0 } /* Guardian */\n";
print $LC "};\n";

close($R);
close($H);
close($A) if defined $A;
close($LC);
close($LH);
