#!/usr/bin/env python
# SPDX-License-Identifier: BSD-3-Clause
#
# simple cpp-style preprocessor
#
# Understands:
#
# #define NAME
#
# Set an option
# You can use -D on the command line too
#
# #undef NAME
#
# Unset an option if it is set
#
# #if .. #endif / #ifdef .. #endif
#
# Specify a list of options set, eg #ifdef DOOM2 || ULTDOOM || SHAREWARE
# The block is only displayed if one of the options is set
#
# #ifn .. #endif / #ifndef .. #endif
#
# Similarly specify a list of options
# The block is displayed if none of the options are set
#
# #include "filename"
#
# include the contents of a file

import sys
import re

debug = False
defines = {}

command_re = re.compile("\#(\w+)(\s+(.*))?")
include_re = re.compile("\s*\"(.*)\"\s*")

def debug_msg(message):
	if debug:
		sys.stderr.write(message)

# Parse command line options

def parse_cmdline():
	for arg in sys.argv[1:]:
		if arg.startswith("-D"):
			name = arg[2:]
			defines[name] = True

def parse_stream(stream):
	result = read_block(stream, False)

	if result is not None:
		raise Exception("Mismatched #if in '%s'" % stream.name)

def parse_file(filename):
	f = open(filename)

	try:
		parse_stream(f)
	finally:
		f.close()

# #include

def cmd_include(arg):
	# Extract the filename

	match = include_re.match(arg)

	if not match:
		raise Exception("Invalid 'include' command")

	filename = match.group(1)

	# Open the file and process it

	parse_file(filename)

# #define

def cmd_define(arg):
	defines[arg] = True

# #undef

def cmd_undef(arg):
	if arg in defines:
		del defines[arg]

# #ifdef/#ifndef

def cmd_ifdef(arg, command, stream, ignore):

	# Get the define name
	name = arg.strip()

	debug_msg("%s %s >\n" % (command, arg))

	# Should we ignore the contents of this block?

	sub_ignore = (name not in defines)

	if "n" in command:
		sub_ignore = not sub_ignore

	# Parse the block

	result = read_block(stream, ignore or sub_ignore)

	debug_msg("%s %s < (%s)\n" % (command, arg, result))

	# There may be a second "else" block to parse:

	if result == "else":
		debug_msg("%s %s else >\n" % (command, arg))
		result = read_block(stream, ignore or (not sub_ignore))
		debug_msg("%s %s else < (%s)\n" % (command, arg, result))

	# Should end in an endif:

	if result != "endif":
		raise Exception("'if' block did not end in an 'endif'")

commands = {
	"include" : cmd_include,
	"define"  : cmd_define,
	"undef"   : cmd_undef,
	"if"      : cmd_ifdef,
	"ifdef"   : cmd_ifdef,
	"ifn"     : cmd_ifdef,
	"ifndef"  : cmd_ifdef,
}

# Recursive block reading function
# if 'ignore' argument is 1, contents are ignored

def read_block(stream, ignore):

	for line in stream:

		# Remove newline

		line = line[0:-1]

		# Ignore empty lines

		if line == " " * len(line):
			continue

		# Check if this line has a command

		match = command_re.match(line)

		if match:
			command = match.group(1)
			arg = match.group(3)

			if command == "else" or command == "endif":
				return command
			elif command not in commands:
				raise Exception("Unknown command: '%s'" % \
						command)

			# Get the callback function.

			func = commands[command]

			# Invoke the callback function. #ifdef commands
			# are a special case and need extra arguments.
			# Other commands are only executed if we are not
			# ignoring this block.

			if func == cmd_ifdef:
				cmd_ifdef(arg, command=command,
					       stream=stream,
					       ignore=ignore)
			elif not ignore:
				func(arg)
		else:
			if not ignore:
				print(line)

parse_cmdline()
parse_stream(sys.stdin)

