public class AVIMetadataLoader {

    private File file = null;

    // A numerical date string, i.e 2010:01:28 14:54:25
    private const int NUMERICAL_DATE_LENGTH = 19;

    // Marker for timestamp section in a Nikon nctg blob.
    private const uint16 NIKON_NCTG_TIMESTAMP_MARKER = 0x13;

    // Size limit to ensure we don't parse forever on a bad file.
    private const int MAX_STRD_LENGTH = 100;

    public AVIMetadataLoader(File file) {
        this.file = file;
    }

    public MetadataDateTime? get_creation_date_time() {
        return new MetadataDateTime(get_creation_date_time_for_avi());
    }

    public string? get_title() {
        // Not supported.
        return null;
    }

    // Checks if the given file is an AVI file.
    public bool is_supported() {
        AVIChunk chunk = new AVIChunk(file);
        bool ret = false;
        try {
            chunk.open_file();
            chunk.read_chunk();
            // Look for the header and identifier.
            if ("RIFF" == chunk.get_current_chunk_name() &&
                "AVI " == chunk.read_name()) {
                ret = true;
            }
        } catch (GLib.Error e) {
            debug("Error while testing for AVI file: %s", e.message);
        }

        try {
            chunk.close_file();
        } catch (GLib.Error e) {
            debug("Error while closing AVI file: %s", e.message);
        }
        return ret;
    }

    // Parses a Nikon nctg tag.  Based losely on avi_read_nikon() in FFmpeg.
    private string read_nikon_nctg_tag(AVIChunk chunk) throws GLib.Error {
        bool found_date = false;
        while (chunk.section_size_remaining() > sizeof(uint16)*2) {
            uint16 tag = chunk.read_uint16();
            uint16 size = chunk.read_uint16();
            if (NIKON_NCTG_TIMESTAMP_MARKER == tag) {
                found_date = true;
                break;
            }
            chunk.skip(size);
        }

        if (found_date) {
            // Read numerical date string, example: 2010:01:28 14:54:25
            GLib.StringBuilder sb = new GLib.StringBuilder();
            for (int i = 0; i < NUMERICAL_DATE_LENGTH; i++) {
                sb.append_c((char) chunk.read_byte());
            }
            return sb.str;
        }
        return "";
    }

    // Parses a Fujifilm strd tag. Based on information from:
    // http://www.eden-foundation.org/products/code/film_date_stamp/index.html
    private string read_fuji_strd_tag(AVIChunk chunk) throws GLib.Error {
        chunk.skip(98); // Ignore 98-byte binary blob.
        chunk.skip(8); // Ignore the string "FUJIFILM"
        // Read until we find four colons, then two more chars.
        int colons = 0;
        int post_colons = 0;
        GLib.StringBuilder sb = new GLib.StringBuilder();
        // End of date is two chars past the fourth colon.
        while (colons <= 4 && post_colons < 2) {
            char c = (char) chunk.read_byte();
            if (4 == colons) {
                post_colons++;
            }
            if (':' == c) {
                colons++;
            }
            if (c.isprint()) {
                sb.append_c(c);
            }
            if (sb.len > MAX_STRD_LENGTH) {
                return ""; // Give up searching.
            }
        }

        if (sb.str.length < NUMERICAL_DATE_LENGTH) {
            return "";
        }
        // Date is now at the end of the string.
        return sb.str.substring(sb.str.length - NUMERICAL_DATE_LENGTH);
    }

    // Recursively read file until the section is found.
    private string? read_section(AVIChunk chunk) throws GLib.Error {
        while (true) {
            chunk.read_chunk();
            string name = chunk.get_current_chunk_name();
            if ("IDIT" == name) {
                return chunk.section_to_string();
            } else if ("nctg" == name) {
                return read_nikon_nctg_tag(chunk);
            } else if ("strd" == name) {
                return read_fuji_strd_tag(chunk);
            }

            if ("LIST" == name) {
                chunk.read_name(); // Read past list name.
                string result = read_section(chunk.get_first_child_chunk());
                if (null != result) {
                    return result;
                }
            }

            if (chunk.is_last_chunk()) {
                break;
            }
            chunk.next_chunk();
        }
        return null;
    }

    // Parses a date from a string.
    // Largely based on GStreamer's avi/gstavidemux.c
    // and the information here:
    // http://www.eden-foundation.org/products/code/film_date_stamp/index.html
    private DateTime? parse_date(string sdate) {
        if (sdate.length == 0) {
            return null;
        }

        int year, month, day, hour, min, sec;
        char weekday[4];
        char monthstr[4];
        DateTime parsed_date;

        if (sdate[0].isdigit()) {
            // Format is: 2005:08:17 11:42:43
            // Format is: 2010/11/30/ 19:42
            // Format is: 2010/11/30 19:42
            string tmp = sdate.dup();
            tmp.canon("0123456789 ", ' '); // strip everything but numbers and spaces
            sec = 0;
            int result = tmp.scanf("%d %d %d %d %d %d", out year, out month, out day, out hour, out min, out sec);
            if(result < 5) {
                return null;
            }

            parsed_date = new DateTime.utc(year, month, day, hour, min, sec);
        } else {
            // Format is: Mon Mar  3 09:44:56 2008
            if(7 != sdate.scanf("%3s %3s %d %d:%d:%d %d", weekday, monthstr, out day, out hour,
                  out min, out sec, out year)) {
                return null; // Error
            }
            parsed_date = new DateTime.local(year, month_from_string((string)monthstr), day, hour, min, sec);
        }

        return parsed_date;
    }

    private DateMonth month_from_string(string s) {
        switch (s.down()) {
        case "jan":
            return DateMonth.JANUARY;
        case "feb":
            return DateMonth.FEBRUARY;
        case "mar":
            return DateMonth.MARCH;
        case "apr":
            return DateMonth.APRIL;
        case "may":
            return DateMonth.MAY;
        case "jun":
            return DateMonth.JUNE;
        case "jul":
            return DateMonth.JULY;
        case "aug":
            return DateMonth.AUGUST;
        case "sep":
            return DateMonth.SEPTEMBER;
        case "oct":
            return DateMonth.OCTOBER;
        case "nov":
            return DateMonth.NOVEMBER;
        case "dec":
            return DateMonth.DECEMBER;
        }
        return DateMonth.BAD_MONTH;
    }

    private DateTime? get_creation_date_time_for_avi() {
        AVIChunk chunk = new AVIChunk(file);
        DateTime? timestamp = null;
        try {
            chunk.open_file();
            chunk.nonsection_skip(12); // Advance past 12 byte header.
            string sdate = read_section(chunk);
            if (null != sdate) {
                timestamp = parse_date(sdate.strip());
            }
        } catch (GLib.Error e) {
            debug("Error while reading AVI file: %s", e.message);
        }

        try {
            chunk.close_file();
        } catch (GLib.Error e) {
            debug("Error while closing AVI file: %s", e.message);
        }
        return timestamp;
    }
}