#!/usr/bin/env python3
## ==========================================================================
#   Licensed under BSD 2clause license See LICENSE file for more information
#   Author: Michał Łyszczek <michal.lyszczek@bofc.pl>
## ==========================================================================

# prefix used to search for default db, can be changed during install step,
# by default it is set to "/usr/local", which is default install directory
# for most manually installed packages.
prefix = "/usr/local"

version="0.2.2"

## ==========================================================================
#                   _                                 __
#                  (_)____ ___   ____   ____   _____ / /_ _____
#                 / // __ `__ \ / __ \ / __ \ / ___// __// ___/
#                / // / / / / // /_/ // /_/ // /   / /_ (__  )
#               /_//_/ /_/ /_// .___/ \____//_/    \__//____/
#                            /_/
## ==========================================================================


import	argparse
import	csv
import	errno
import	os
import	pprint
import	re
import	sys

from	string import Template


## ==========================================================================
#               ____                     __   _
#              / __/__  __ ____   _____ / /_ (_)____   ____   _____
#             / /_ / / / // __ \ / ___// __// // __ \ / __ \ / ___/
#            / __// /_/ // / / // /__ / /_ / // /_/ // / / /(__  )
#           /_/   \__,_//_/ /_/ \___/ \__//_/ \____//_/ /_//____/
#
## ==========================================================================


## ==========================================================================
#   Prints message to standard error
## ==========================================================================


def eprint(
	*args,
	**kwargs
):
	print(*args, file=sys.stderr, **kwargs)

## ==========================================================================
#   Function removes conditionals from specified file. Conditional must be
#   in format %{if <bool>} (...) %{fi}, and if <bool> is 0, all data between
#   %{if} and %{fi} will be removed. Here are some examples.
#
#   example 1:
#       %{if 0}
#       some code
#       more code
#       %{fi}
#       different cnode
#
#   will return
#       different code
#
#   while example 2:
#       %{if 1}
#       some code
#       more code
#       %{fi}
#       different cnode
#
#   will return:
#       some code
#       more code
#       different code
#
#   This function is stupid and does not work with any variables, it is
#   designed to work with stirng.subsitute(), so if you want variables use
#   %{if ${some_variable}} and run the file through string.substitute()
#   to change ${some_variable> to 0 or 1, and then run this function.
#
#   %{if} can be in the middle of string like
#
#   example 3:
#       if (%{if 1}some_check ||%{fi} another_check)
## ==========================================================================


def process_conditionals(
	content         # string on which to perform conditional removal
):
	inside_cond = 0 # flag to know if we are inside %{if} or not
	cond_true = 0   # value of %{if} conditional
	out = ""        # output file after processing
	i = 0           # current file position
	#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#

	while i < len(content):
		if content[i] == '\n':
			line_started = 1
			out += content[i]
			i += 1
			continue

		if inside_cond == 0:
			# we are outside of conditional section, check if we are
			# about to enter one
			if not content[i: i + 5].startswith("%{if ") or \
					content[i + 6] != "}":
				# nope, current position is not "%{if " or there is no
				# closing "}" character, so this is not conditional
				line_started = 0
				out += content[i]
				i += 1
				continue

			# yes, content[i] is "%{if 1}" or "%{if 0}" we are dealing
			# with conditional, read conditional value and set flag
			# to know we are inside conditional now
			cond_true = content[i + 5]
			inside_cond = 1

			# obviously we do not store conditional in out, so skip it
			i += 7

			if (i == 7 or content[i - 7 - 1] == "\n") and content[i] == "\n":
				# unique situation, %{if N} is the only text in the
				# line, so let's also remove a new line character
				# that follows it
				i += 1
		else:
			# we are inside conditional, let's look for closing %{fi}
			# statement

			if not content[i: i + 5].startswith("%{fi}"):
				# does not look like and conditonal ending, but what
				# should we do with data inside conditional?

				if int(cond_true) != 0:
					# true, we should store that content in out,
					# otherwise we do nothing, data is not stored
					# and we only increment i
					out += content[i]

				i += 1
				continue

			# we got ending "%{fi}", so simply skip it and mark
			# we are out of conditional
			i += 5
			inside_cond = 0

			if content[i - 5 - 1] == "\n" and content[i] == "\n":
				# unique situation, %{fi} is the only text in the
				# line, so let's also remove a new line character
				# that follows it
				i += 1
				continue

			if int(cond_true) == 0 and line_started and content[i] == "\n":
				# remove whole line if the only thing we have there
				# is conditional and that conditional is false, to
				# avoid printing unnecessary blank lines.
				i += 1

	# parsing is done, return processed data
	return out


## ==========================================================================
#                                 __                __
#                          _____ / /_ ____ _ _____ / /_
#                         / ___// __// __ `// ___// __/
#                        (__  )/ /_ / /_/ // /   / /_
#                       /____/ \__/ \__,_//_/    \__/
#
## ==========================================================================


    ## ==================================================================
    #            __       __                      __   __
    #        ___/ /___ _ / /_ ___ _  ___ _ ___ _ / /_ / /  ___  ____
    #       / _  // _ `// __// _ `/ / _ `// _ `// __// _ \/ -_)/ __/
    #       \_,_/ \_,_/ \__/ \_,_/  \_, / \_,_/ \__//_//_/\__//_/
    #                              /___/
    ## ==================================================================


# read arguments to get data about config to generate
vstr = "fogen v" + version
parser = argparse.ArgumentParser(description="Generate fo.{c,h} files")
parser.add_argument("-v", "--version", action='version', version=vstr)
parser.add_argument("-d", "--db", required=0, metavar="file",
		help="Path to a csv database file with functions information " + \
				"that can be used to generage fo.c file. If not passed " + \
				"default db will be used")
parser.add_argument("-o", "--output", required=0, metavar="name",
		help="set output filename (without extension), if not specified " + \
				"fo will be used to generate fo.c and fo.h files")
parser.add_argument("-l", "--list", required=0, metavar="file",
		help="new-line separated list of function to generate fo.c file " + \
				"with, if not specified, all functions from db will be " + \
				"generated")
parser.add_argument("-r", "--res-dir", required=0, metavar="path",
		help="Path to custom resource directory")
args = parser.parse_args()


resdir = prefix + "/share/fogen"
# if system directory does not exist (ie. progen was not
# installed and is runs directly from source directory, use
# resources where script is located
if not os.path.isdir(resdir):
	resdir = os.path.dirname(os.path.realpath(__file__))
if args.res_dir:
	resdir = args.res_dir

db_path = resdir + "/db.conf"
if args.db:
	db_path = args.db

outc = "fo.c"
outh = "fo.h"
if args.output:
	outc = args.output + ".c"
	outh = args.output + ".h"

flist = ""
if args.list:
	flist = args.list

eprint("fogen - starting")
eprint("functions database file..: " + db_path)
eprint("output files.............: " + outc + ", " + outh)
eprint("function list file.......: " + \
		("none, generate all from db" if len(flist) == 0 else flist))
eprint("resource directory.......: " + resdir)

dbc = ""
with open(db_path) as file:
	for line in file:
		if line.startswith("#") or len(line) == 0:
			continue
		dbc += line

db = []
dbt = dict()
dbt = [row for row in csv.DictReader(dbc.splitlines(), skipinitialspace=1)]

fo_fgets_used = 0
fo_strerror_used = 0
fo_atoi_used = 0
fo_fclose_used = 0
fo_feof_used = 0
fo_strcmp_used = 0
fo_atol_used = 0
fo_memset_used = 0
used_funcs = []


if len(flist) == 0:
	db = dbt
	for opt in dbt:
		used_funcs.append(opt['fname'])
else:
	with open(flist) as fl:
		used_funcs = fl.read().splitlines()
		for opt in dbt:
			fl.seek(0)
			for f in fl.read().splitlines():
				if f == opt['fname']:
					db.append(opt)
					break



for f in used_funcs:
	if f == "fgets":
		fo_fgets_used = 1
	elif f == "strerror":
		fo_strerror_used = 1
	elif f == "atoi":
		fo_atoi_used = 1
	elif f == "fclose":
		fo_fclose_used = 1
	elif f == "feof":
		fo_feof_used = 1
	elif f == "strcmp":
		fo_strcmp_used = 1
	elif f == "atol":
		fo_atol_used = 1
	elif f == "memset":
		fo_memset_used = 1


    ## ==================================================================
    #                                   __                   __        __
    #  ___ _ ___  ___  ___  ____ ___ _ / /_ ___   ___ __ __ / /   ___ / /_
    # / _ `// -_)/ _ \/ -_)/ __// _ `// __// -_) (_-</ // // _ \ (_-</ __/
    # \_, / \__//_//_/\__//_/   \_,_/ \__/ \__/ /___/\_,_//_.__//___/\__/
    #/___/
    ## ==================================================================
    #   Now generate substitutions so they can be later use to replace
    #   placeholders in fo.{c,h}.in files.
    ## ==================================================================


## ==========================================================================
#   ${include_list}
#
#   like:
#       #include <stdlib.h>
#       #include <stdio.h>
#       #include <unistd.h>
## ==========================================================================


headers = ["ctype.h", "dlfcn.h", "errno.h", "stdint.h", "stdio.h", "stdlib.h",
		"string.h"]
for f in db:
	for h in f['headers'].split(";"):
		headers.append(h.strip())

# remove duplicates
headers = list(set(headers))

include_list = ""
for h in headers:
	include_list += "#include <" + h + ">\n"


## ==========================================================================
#    ${fo_init_list}
#    ${fo_enum_list}
#
#    like:
#        FO_INIT(setsockopt);
#        FO_INIT(read);
#        FO_INIT(close);
## ==========================================================================


fo_init_list = ""
fo_enum_list = ""
for f in db:
	fo_init_list += "\tFO_INIT({});\n".format(f['fname'])
	fo_enum_list += "\tfo_{},\n".format(f['fname'])


## ==========================================================================
#   ${fo_overrides}
#
#   all overriden functions
## ==========================================================================


o = ""
for f in db:
	o += "{} {}\n(\n".format(f['rtype'], f['fname'])

	i = 0
	args = ""       # <type> <name> args in function decl
	types = ""      # types for function pointer decl <type1>, <type2>, ...
	args_call = ""  # args in call: arg1, arg2, arg3, ...
	for t in f['args'].split(";"):
		t = t.strip()
		args += "\t{} arg{},\n".format(t, i)
		types += "{}, ".format(t)
		args_call += "arg{}, ".format(i)
		i += 1

	# strip last commas
	args = args[:-2]
	types = types[:-2]
	args_call = args_call[:-2]

	o += args + "\n)\n{\n"
	o += "\tif (fo_info[fo_{}].countdown == -1 ||\n".format(f['fname'])
	o += "\t\t\t--(fo_info[fo_{}].countdown) != 0)\n".format(f['fname'])
	o += "\t{\n"
	o += "\t\t{} (*original)({});\n".format(f['rtype'], types)
	o += "\t\toriginal = fo_info[fo_{}].original;\n".format(f['fname'])
	o += "\t\treturn original({});\n".format(args_call)
	o += "\t}\n"

	try:
		with open(resdir + "/custom/" + f['fname']) as c:
			o += "\n"
			for l in c.read().splitlines():
				o += "\t{}\n".format(l)
			o += "\n"
	except:
		pass

	o += "\terrno = fo_info[fo_{}].errn;\n".format(f['fname'])
	o += "\treturn ({})fo_info[fo_{}].ret;\n".format(f['rtype'], f['fname'])
	o += "}\n\n\n"

fo_overrides = o[:-3] # strip trailing new lines


    ## ==================================================================
    #           __                     __       __   _
    #          / /_ ___  __ _   ___   / /___ _ / /_ (_)___  ___ _
    #         / __// -_)/  ' \ / _ \ / // _ `// __// // _ \/ _ `/
    #         \__/ \__//_/_/_// .__//_/ \_,_/ \__//_//_//_/\_, /
    #                        /_/                          /___/
    ## ==================================================================
    #   Now that all templates are prepared, we can replace data in
    #   `.in' files with generated ones.
    ## ==================================================================


with open(resdir + "/fo.c.in") as file_in, open(outc, "w") as file_out:
	d = dict()

	# create dict with all substitutions
	d['include_list'] = include_list
	d['fo_init_list'] = fo_init_list
	d['fo_overrides'] = fo_overrides

	d['fo_fgets_used'] = fo_fgets_used
	d['fo_strerror_used'] = fo_strerror_used
	d['fo_atoi_used'] = fo_atoi_used
	d['fo_fclose_used'] = fo_fclose_used
	d['fo_feof_used'] = fo_feof_used
	d['fo_strcmp_used'] = fo_strcmp_used
	d['fo_atol_used'] = fo_atol_used
	d['fo_memset_used'] = fo_memset_used

	# and replace all
	content = Template(file_in.read()).substitute(d)

	# now replace all the conditionals
	file_out.write(process_conditionals(content))


with open(resdir + "/fo.h.in") as file_in, open(outh, "w") as file_out:
	d = dict()

	# create dict with all substitution
	d['fo_enum_list'] = fo_enum_list

	# and replace all
	file_out.write(Template(file_in.read()).substitute(d))
