/*
 * DBFExporter.java
 *
 * Created on April 13, 2007, 2:18 PM
 *
 */
package edu.oregonstate.cartography.geometryexport;

import edu.oregonstate.cartography.simplefeatures.Geometry;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.text.DecimalFormat;
import java.util.Calendar;
import java.util.GregorianCalendar;

/**
 * Exporter for the DBF file format.
 *
 * @author Bernhard Jenny, Institute of Cartography, ETH Zurich.
 */
public class DBFExporter {

    private static final int STRING_LENGTH = 64;
    private static final int NUMBER_LENGTH = 20;    // F n=1..20
    private static final int NUMBER_DECIMALS = 8;

    /**
     * Creates a new instance of DBFExporter
     */
    public DBFExporter() {
    }

    public void exportTable(OutputStream outputStream, Geometry[] geometries, String attributeName)
            throws IOException {

        BufferedOutputStream bos = null;
        try {
            bos = new BufferedOutputStream(outputStream);
            LittleEndianOutputStream dos = new LittleEndianOutputStream(bos);

            // dbf flag
            dos.write(0x03);

            // current date
            Calendar cal = GregorianCalendar.getInstance();
            int year = cal.get(Calendar.YEAR);             // 2002
            int month = cal.get(Calendar.MONTH);           // 0=Jan, 1=Feb, ...
            int day = cal.get(Calendar.DAY_OF_MONTH);      // 1...
            dos.write(year - 1900);
            dos.write(month);
            dos.write(day);

            // number of records
            dos.writeInt(geometries.length);

            // header size
            int columnsCount = 1; // table.getColumnCount();
            if (attributeName != null) {
                columnsCount++;
            }
            dos.writeShort(32 + columnsCount * 32 + 1);

            // record size
            dos.writeShort(computeRecordSize(null));

            // reserved value, fill with 0
            dos.writeShort(0);

            // transaction byte
            dos.write(0);

            // encription byte
            dos.write(0);

            // multi user environment use
            for (int i = 0; i < 13; i++) {
                dos.write(0);
            }

            // codepage / language driver
            // ESRI shape files use code 0x57 to indicate that
            // data is written in ANSI (whatever that means).
            // http://www.esricanada.com/english/support/faqs/arcview/avfaq21.asp
            dos.write(0x57);

            // two reserved bytes
            dos.writeShort(0);

            this.writeFieldDescriptors(dos, attributeName);

            // header record terminator
            dos.write(0x0D);

            this.writeRecords(dos, geometries, attributeName);
        } finally {
            if (bos != null) {
                bos.close();
            }
        }
    }

    private int computeRecordSize(String attributeName) throws IOException {
        int recordSize = 1; // 1 for deletion flag
        int columnsCount = 1; // table.getColumnCount();
        if (attributeName != null) {
            columnsCount++;
        }
        for (int i = 0; i < columnsCount; i++) {
            /*TableColumn tc = table.getColumn(i);
            
             if (table.isStringColumn(i)) {
             recordSize += STRING_LENGTH;
             } else if (table.isDoubleColumn(i)) {
             recordSize += NUMBER_LENGTH;
             } else {
             throw new IOException("DBF export: unsupported type");
             }*/
            recordSize += NUMBER_LENGTH;
        }
        return recordSize;
    }

    private void writeString(LittleEndianOutputStream dos, String str, int length)
            throws IOException {
        byte[] b = str.getBytes("ISO-8859-1");
        dos.write(b, 0, Math.min(length, b.length));
        for (int c = b.length; c < length; c++) {
            dos.write(0);
        }
    }

    private void writeFieldDescriptors(LittleEndianOutputStream dos, String attributeName)
            throws IOException {

        int columnsCount = 1; // table.getColumnCount();
        if (attributeName != null) {
            columnsCount++;
        }
        for (int i = 0; i < columnsCount; i++) {
            //TableColumn tc = table.getColumn(i);

            // write column title, 10 chars, plus terminating 0.
            String title = "attr" + i; // tc.getHeaderValue().toString().trim();
            this.writeString(dos, title, 10);
            dos.write(0x0);

            // write field type
            /*if (table.isStringColumn(i)) {
             dos.write('C');
             dos.writeInt(0);            // field address (ignored)
             dos.write(STRING_LENGTH);   // field length
             dos.write(0);               // decimal count not used
             } else if (table.isDoubleColumn(i)) {*/
            dos.write('F');
            dos.writeInt(0);            // field address (ignored)
            dos.write(NUMBER_LENGTH);   // field length
            dos.write(NUMBER_DECIMALS); // decimal count
           /* } else {
             throw new IOException("DBF export: unsupported type");
             }*/

            // 14 reserved or unusued bytes
            for (int c = 0; c < 14; c++) {
                dos.write(0);
            }
        }
    }

    /**
     * Convert a double value to a string with a specified width, number of
     * digits after the decimal point, and preceeding white spaces.
     *
     * @param value The value to convert.
     * @param width The total width of the string (= the number of characters).
     * @param decimals The number of digits after the decimal point.
     * @param leadingSpaces The number of leading spaces. Usually 0.
     * @return The value converted to a string.
     */
    public static String format(double value, int width, int decimals, int leadingSpaces) {
        StringBuilder str = new StringBuilder(width);
        width += 1 + leadingSpaces;
        DecimalFormat formatter = new DecimalFormat();
        formatter.setMaximumFractionDigits(decimals);
        formatter.setMinimumFractionDigits(decimals);
        String s = formatter.format(value); // format the number
        int padding = Math.max(leadingSpaces, width - s.length()); // insert leadingSpaces
        for (int k = 0; k < padding; k++) {
            str.append(' ');
        }
        str.append(s);
        return str.toString();
    }

    private void writeRecords(LittleEndianOutputStream dos, Geometry[] geometries, String attributeName)
            throws IOException {

        int rowsCount = geometries.length; // table.getRowCount();
        for (int row = 0; row < rowsCount; row++) {
            // write deleted flag
            dos.write(' ');

            String nbrStr = format(row, NUMBER_LENGTH, NUMBER_DECIMALS, 0);
            this.writeString(dos, nbrStr, NUMBER_LENGTH);
            
            Number attr = geometries[row].getAttribute(attributeName);
            if (attr != null) {
                nbrStr = format(attr.doubleValue(), NUMBER_LENGTH, NUMBER_DECIMALS, 0);
            } else {
                nbrStr = format(0, NUMBER_LENGTH, NUMBER_DECIMALS, 0);
            }
            this.writeString(dos, nbrStr, NUMBER_LENGTH);
        }
    }
}
