/*
 * csv.d
 *	Lightweight implementation of a CSV parser
 */
module tiny.csv;

import std.stdio : File;

// Read CSV lines from a file into the struct of type T
struct CSVreader(T,F=File) {
private:
    import tiny.ctype : isdigit;
    import std.exception : enforce;

    F f;
    char sep = ',';
    bool have_elem = false;
    bool at_eof = false;
    string curline;
    T elem;

    // Tell if we've consumed all of the current line
    bool at_eol() {
	return this.curline.length == 0;
    }

    // Get next char from input stream
    char nextc() {
	enforce(!this.at_eol(), "Short CSV line");
	char c = this.curline[0];
	this.curline = this.curline[1 .. $];
	return c;
    }

    // Get integer CSV column value
    int get_int() {
	auto c = this.nextc();
	enforce(isdigit(c), "Bad digits in CSV numeric field");
	int res = c - '0';
	while (true) {
	    // Last field in CSV input line
	    if (this.at_eol()) {
		return res;
	    }

	    // Another digit?
	    c = this.nextc();
	    if (isdigit(c)) {
		res *= 10;
		res += (c - '0');
		continue;
	    }

	    // Otherwise must be comma leading to next field
	    enforce(c == this.sep, "Bad digits in CSV numeric field");

	    // Here's your assembled value
	    return res;
	}
    }

    // Get string CSV column value
    string get_str() {
	auto res = "";
	while (true) {
	    // End of line
	    if (this.at_eol()) {
		return res;
	    }
	    auto c = this.nextc();

	    // Field separator
	    if (c == this.sep) {
		return res;
	    }
	    res ~= c;
	}
    }

    // If needed, go get and decode the next line
    void refill() {

	// We have a current element
	if (this.have_elem) {
	    return;
	}

	// All out
	if (this.at_eof) {
	    return;
	}

	// Otherwise we need to go get the next one
	this.curline = this.f.readln();
	if (this.curline.length == 0) {
	    // At EOF now
	    this.at_eof = true;
	    return;
	}

	auto t = T();
	foreach (i, ref val; t.tupleof) {
	    static if (is(typeof(val) == int)) {
		val = this.get_int();
	    } else {
		val = this.get_str();
	    }
	}

	// Trailing fields?
	enforce(this.at_eol(), "Trailing fields");

	// New current element
	this.elem = t;
	this.have_elem = true;
    }

public:
    // Is input stream empty?
    bool empty() {
	// We have the next element available
	if (this.have_elem) {
	    return false;
	}

	// Get the next one?
	this.refill();
	return (!this.have_elem);
    }

    // Drop current
    void popFront() {
	this.have_elem = false;
    }

    // Current element
    T front() {
	// They must check empty() first
	assert(this.have_elem);
	return this.elem;
    }

}

unittest {
    struct Test {
	int a;
	string b;
    }
    import tiny.strfile : StrFile;
    auto f = StrFile("123,b\n", "r");
    auto c = new CSVreader!(Test,StrFile)(f);
    assert(!c.empty());
    auto t1 = c.front();
    assert(t1.a == 123);
    assert(t1.b == "b");
    c.popFront();
    assert(c.empty());
}
version(unittest) {
    void
    main()
    {
    }
}
