/* valaccodewriter.vala
 *
 * Copyright (C) 2006-2009  Jürg Billeter
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.

 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.

 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301  USA
 *
 * Author:
 * 	Jürg Billeter <j@bitron.ch>
 */

using GLib;

/**
 * Represents a writer to write C source files.
 */
public class Vala.CCodeWriter {
	/**
	 * Specifies the file to be written.
	 */
	public string filename { get; set; }

	/**
	 * Specifies the source file used to generate this one.
	 */
	private string source_filename;

	/**
	 * Specifies whether to emit line directives.
	 */
	public bool line_directives { get; set; }

	/**
	 * Specifies whether the output stream is at the beginning of a line.
	 */
	public bool bol {
		get { return _bol; }
	}

	static GLib.Regex fix_indent_regex;

	private string temp_filename;
	private bool file_exists;

	private FileStream? stream;

	private int indent;
	private int current_line_number = 1;
	private bool using_line_directive;

	/* at begin of line */
	private bool _bol = true;
	/* at begin after empty line */
	private bool _bael = false;

	public CCodeWriter (string filename, string? source_filename = null) {
		this.filename = filename;
		this.source_filename = source_filename;
	}

	/**
	 * Opens the file.
	 *
	 * @return true if the file has been opened successfully,
	 *         false otherwise
	 */
	public bool open (bool write_version) {
		file_exists = FileUtils.test (filename, FileTest.EXISTS);
		if (file_exists) {
			temp_filename = "%s.valatmp".printf (filename);
			stream = FileStream.open (temp_filename, "w");
		} else {
			/*
			 * File doesn't exist. In case of a particular destination (-d flag),
			 * check and create the directory structure.
			 */
			var dirname = Path.get_dirname (filename);
			DirUtils.create_with_parents (dirname, 0755);
			stream = FileStream.open (filename, "w");
		}

		if (stream == null) {
			return false;
		}

		var opening = write_version ?
			"/* %s generated by valac %s, the Vala compiler".printf (Path.get_basename (filename), Vala.BUILD_VERSION) :
			"/* %s generated by valac, the Vala compiler".printf (Path.get_basename (filename));
		write_string (opening);

		// Write the file name if known
		if (source_filename != null) {
			write_newline ();
			write_string (" * generated from %s".printf (Path.get_basename (source_filename)));
		}

		write_string (", do not modify */");
		write_newline ();
		write_newline ();

		return true;
	}

	/**
	 * Closes the file.
	 */
	public void close () {
		stream = null;

		if (file_exists) {
			var changed = true;

			try {
				var old_file = new MappedFile (filename, false);
				var new_file = new MappedFile (temp_filename, false);
				var len = old_file.get_length ();
				if (len == new_file.get_length ()) {
					if (Memory.cmp (old_file.get_contents (), new_file.get_contents (), len) == 0) {
						changed = false;
					}
				}
				old_file = null;
				new_file = null;
			} catch (FileError e) {
				// assume changed if mmap comparison doesn't work
			}

			if (changed) {
				FileUtils.rename (temp_filename, filename);
			} else {
				FileUtils.unlink (temp_filename);
				if (source_filename != null) {
					var stats = Stat (source_filename);
					var target_stats = Stat (filename);
					if (stats.st_mtime >= target_stats.st_mtime) {
						UTimBuf timebuf = { stats.st_atime + 1, stats.st_mtime + 1 };
						FileUtils.utime (filename, timebuf);
					}
				}
			}
		}
	}

	/**
	 * Writes tabs according to the current indent level.
	 */
	public void write_indent (CCodeLineDirective? line = null) {
		if (line_directives) {
			if (line != null) {
				line.write (this);
				using_line_directive = true;
			} else if (using_line_directive) {
				// no corresponding Vala line, emit line directive for C line
				write_string ("#line %d \"%s\"".printf (current_line_number + 1, Path.get_basename (filename)));
				write_newline ();
				using_line_directive = false;
			}
		}

		if (!_bol) {
			write_newline ();
		}

		stream.puts (string.nfill (indent, '\t'));
		_bol = false;
	}

	/**
	 * Writes n spaces.
	 */
	public void write_nspaces (uint n) {
		stream.puts (string.nfill (n, ' '));
	}

	/**
	 * Writes the specified string.
	 *
	 * @param s a string
	 */
	public void write_string (string s) {
		stream.puts (s);
		_bol = false;
	}

	/**
	 * Writes a newline.
	 */
	public void write_newline () {
		if (!_bol) {
			_bael = false;
		} else if (!_bael) {
			_bael = true;
		} else {
			return;
		}
		stream.putc ('\n');
		current_line_number++;
		_bol = true;
	}

	/**
	 * Opens a new block, increasing the indent level.
	 */
	public void write_begin_block () {
		if (!_bol) {
			stream.putc (' ');
		} else {
			write_indent ();
		}
		stream.putc ('{');
		write_newline ();
		indent++;
	}

	/**
	 * Closes the current block, decreasing the indent level.
	 */
	public void write_end_block () {
		assert (indent > 0);

		indent--;
		write_indent ();
		stream.putc ('}');
	}

	/**
	 * Writes the specified text as comment.
	 *
	 * @param text the comment text
	 */
	public void write_comment (string text) {
		try {
			write_indent ();
			stream.puts ("/*");
			bool first = true;

			// discard tabs at beginning of line
			if (fix_indent_regex == null)
				fix_indent_regex = new GLib.Regex ("^\t+");;

			foreach (unowned string line in text.split ("\n")) {
				if (!first) {
					write_indent ();
				} else {
					first = false;
				}

				var lineparts = fix_indent_regex.replace_literal (line, -1, 0, "").split ("*/");

				for (int i = 0; lineparts[i] != null; i++) {
					stream.puts (lineparts[i]);
					if (lineparts[i+1] != null) {
						stream.puts ("* /");
					}
				}
			}
			stream.puts ("*/");
			write_newline ();
		} catch (RegexError e) {
			// ignore
		}
	}
}
