#!/usr/bin/env python3

import shlex
import os
import sys
import argparse
import re

help_str = """

Tool for building file lists, into formats required by various tools.
".f" files contain four types of command:

file (filename)
    adds a file to the list
include (dir)
	add a directory to include path, if output format supports this
wildcard (.extension) (dir)
    add all files with a given extension in a given directory
list (filename)
    recurse on another filelist file

The idea is that each component in a project has a 
.f file which lists all of the Verilog (e.g.) files
for that component. Higher-level .f files will hierarchically
include the lower-level ones.

In this way, you can build large flat file lists to pass into the
various tools, but never have to *write* large flat file lists.

It also makes it easier to specify parts of your design hierarchy
for a given tool. For example, if you just want to synthesise your
CPU, you can run "listfiles cpu.f -f flat"
"""

def wildcard(dir, extension):
	prev_dir = os.getcwd()
	os.chdir(dir)
	files = [os.path.abspath(f) for f in os.listdir() if os.path.splitext(f)[-1] == extension]
	os.chdir(prev_dir)
	return files

def read_filelist(fname):
	files = []
	includes = []
	f = open(fname)
	prev_dir = os.getcwd()
	os.chdir(os.path.dirname(os.path.abspath(fname)))
	for l in f.readlines():
		l = l.split("#")[0].strip()
		if l == "":
			continue
		words = shlex.split(l)
		if words[0] == "file":
			assert(len(words) == 2)
			files.append(os.path.abspath(os.path.expandvars(words[1])))
		elif words[0] == "include":
			assert(len(words) == 2)
			includes.append(os.path.abspath(os.path.expandvars(words[1])))
		elif words[0] == "wildcard":
			assert(len(words) == 3)
			files.extend(wildcard(os.path.expandvars(words[2]), words[1]))
		elif words[0] == "list":
			assert(len(words) == 2)
			newfiles, newincludes = read_filelist(os.path.expandvars(words[1]))
			files.extend(newfiles)
			includes.extend(newincludes)
		else:
			raise Exception("In filelist {}: Invalid command \"{}\"".format(fname, words[0]))
	os.chdir(prev_dir)
	return (files, includes)

formats = {
	"isim":  lambda f, i: "verilog work {} {}\n".format(" ".join(f), " ".join("-i " + inc for inc in i)),
	"flat":  lambda f, i: " ".join(f) + "\n",
	"flati": lambda f, i: " ".join(i) + "\n",
	"make":  lambda f, i: "SRCS={}\nINCDIRS={}\n".format(" ".join(f), " ".join(i))
}

if __name__ == "__main__":
	parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter)
	parser.epilog = help_str
	parser.add_argument("src", help="File list source file")
	parser.add_argument("--format", "-f", help="Format to generate output in. Allowed: isim, make, flat (default)")
	parser.add_argument("--relative", "-r", action="store_true", help="Use relative paths in output file")
	parser.add_argument("--relativeto", help="Use relative paths, relative to some specified path")
	parser.add_argument("--output", "-o", help="Output file name")
	args = parser.parse_args()
	if args.format is None:
		args.format = "flat"
	files, includes = read_filelist(args.src)
	# Uniquify whilst preserving order
	func = lambda l: list(dict.fromkeys(l))
	if args.relative:
		func = lambda l, func=func: [os.path.relpath(f) for f in func(l)]
	elif args.relativeto:
		func = lambda l, func=func: [os.path.relpath(f, args.relativeto) for f in func(l)]

	files, includes = map(func, (files, includes))
	if args.format not in formats:
		sys.exit("Unknown format: " + args.format)
	ofile = sys.stdout if args.output is None else open(args.output, "w")
	ofile.write(formats[args.format](files, includes))
