1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
//! Generate boilerplate code, run from build.rs

use std::collections::HashMap;
use std::env;
use std::error::Error;
use std::fs;
use std::fs::File;
use std::io::Write;
use std::mem;
use std::path::Path;
use std::path::PathBuf;

mod year;

/// Entry point for code generation
///
/// # Errors
///
/// Returns any errors from the underlying IO operations unchanged
///
/// # Panics
///
/// If the package name does not conform to expectations.
///
#[ allow (clippy::missing_inline_in_public_items) ]
#[ allow (clippy::print_stdout) ]
pub fn invoke () -> Result <(), Box <dyn Error>> {
	println! ("cargo:rerun-if-changed=build.rs");
	println! ("cargo:rerun-if-changed=.");
	let pkg_name = env::var ("CARGO_PKG_NAME") ?;
	if pkg_name == "aoc-common" {
		let home = env::current_dir () ?;
		for year in ["2015", "2021"] {
			env::set_current_dir (format! ("../{year}")) ?;
			year::prepare (year) ?;
			env::set_current_dir (& home) ?;
		}
	} else {
		let pkg_name_parts: Vec <& str> = pkg_name.split ('-').collect ();
		if pkg_name_parts.len () < 2 { Err ("Invalid package name") ? }
		if pkg_name_parts [0] != "aoc" { Err ("Invalid package name") ? }
		let year = pkg_name_parts [1];
		if pkg_name_parts.len () == 2 {
			year::prepare (year) ?;
		} else if pkg_name_parts.len () == 4 {
			if pkg_name_parts [2] != "day" { Err ("Invalid package name") ? }
			let day = pkg_name_parts [3];
			prepare_day (year, day) ?;
		} else { Err ("Invalid package name") ? }
	}
	Ok (())
}

/// Generate code for a single day
///
fn prepare_day (year: & str, day: & str) -> Result <(), Box <dyn Error>> {
	let src_path = PathBuf::from (if Path::new ("src/").exists () { "src/" } else { "./" });
	let mut main_path = src_path;
	main_path.push ("main.rs");
	write_file (
		main_path,
		replace_placeholders (templates::DAY_MAIN, & HashMap::from_iter (vec! [
			("${YEAR}", year),
			("${DAY}", day),
		])),
	) ?;
	Ok (())
}

/// Write the provided lines to a named file
///
fn write_file (
	name: impl AsRef <Path>,
	lines: impl IntoIterator <Item = impl AsRef <str>>,
) -> Result <(), Box <dyn Error>> {
	let mut new_contents = String::new ();
	for line_temp in lines {
		let line = line_temp.as_ref ();
		new_contents.push_str (line);
		new_contents.push ('\n');
	}
	let old_contents = fs::read_to_string (& name).unwrap_or_default ();
	if old_contents != new_contents {
		let mut main_rs_file = File::create (name) ?;
		write! (& mut main_rs_file, "{new_contents}") ?;
	}
	Ok (())
}

/// Replace placeholders in some strings
///
fn replace_placeholders (lines: & [& str], replacements: & HashMap <& str, & str>) -> Vec <String> {
	lines.iter ().map (|line| {
		let (output, buffer) = line.chars ().fold ((String::new (), String::new ()),
			|(mut output, mut buffer), letter| {
				if (buffer.is_empty () && letter == '$')
						|| (buffer.len () == 1 && letter == '{')
						|| buffer.len () > 1 {
					buffer.push (letter);
					if letter == '}' {
						let replacement = replacements.get (buffer.as_str ()).unwrap_or_else (||
							panic! ("Replacement not found for: {buffer}"),
						);
						output.push_str (replacement);
						buffer = String::new ();
					}
				} else {
					output.push_str (mem::take (& mut buffer).as_str ());
					output.push (letter);
				}
				(output, buffer)
			},
		);
		if ! buffer.is_empty () { panic! () }
		output
	}).collect ()
}

/// Templates for generated file contents
///
mod templates {

	/// Template for main.rs in a day
	///
	pub const DAY_MAIN: & [& str] = & [
		"use std::env;",
		"use std::ffi::OsString;",
		"",
		"use aoc_common::*;",
		"use aoc_${YEAR}_day_${DAY}::*;",
		"",
		"fn main () -> GenResult <()> {",
		"	let args: Vec <OsString> = env::args_os ().collect ();",
		"	puzzle_metadata ().invoke (& args)",
		"}",
	];

}