package org.autoplot.hapiserver;

import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.text.ParseException;
import java.util.Arrays;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.das2.datum.Datum;
import org.das2.datum.Units;
import org.das2.datum.UnitsUtil;
import org.das2.datum.format.DatumFormatter;
import org.das2.datum.format.EnumerationDatumFormatter;
import org.das2.datum.format.FormatStringFormatter;
import org.das2.datum.format.TimeDatumFormatter;
import org.das2.qds.DDataSet;
import org.json.JSONObject;
import org.das2.qds.DataSetUtil;
import org.das2.qds.QDataSet;
import org.das2.qds.SparseDataSetBuilder;
import org.das2.qds.WritableDataSet;
import org.json.JSONArray;
import org.json.JSONException;

/**
 * Comma Separated Value (CSV) formatter
 * @author jbf
 */
public class CsvDataFormatter implements DataFormatter {

    private static final Logger logger= Logger.getLogger("hapi.csv");
    
    boolean[] unitsFormatter;
    DatumFormatter[] datumFormatter;
    
    /**
     * true if the field needs to be quoted.
     */
    boolean[] quotes;
    
    /**
     * the lengths of each field, for isotime and string types.
     */
    int[] lengths;
    String[] fill;
    Units[] units;
    
    private static final Charset CHARSET_UTF8= Charset.forName("UTF-8");
    
    /**
     * @see AsciiTableDataSourceFormat#getDataFormatter
     * @param df format, such as %.2f or %d
     * @param u the units to fall back on.
     * @return the DatumFormatter.
     */
    private DatumFormatter getDataFormatter( String df, Units u ) {
        try {
            if ( !df.contains("%") ) df= "%"+df;
            //TODO: would be nice if we could verify formatter.  I had %f5.2 instead of %5.2f and it wasn't telling me.
            return new FormatStringFormatter( df, false );
        } catch ( RuntimeException ex ) {
            logger.log( Level.SEVERE, ex.getMessage(), ex);
            return u.getDatumFormatterFactory().defaultFormatter();
        }
    }
    
    /**
     * return the parameter number for the column.
     * @param col
     * @return 
     */
    int columnMap( int col ) {
        return col;
    }
    
    @Override
    public void initialize( JSONObject info, OutputStream out, QDataSet record) {
        try {
            unitsFormatter= new boolean[record.length()];
            datumFormatter= new DatumFormatter[record.length()];
            quotes= new boolean[record.length()];
            lengths= new int[record.length()];
            units= new Units[record.length()];
            fill= new String[record.length()];
            int[] lens= Util.getNumberOfElements(info);
            JSONArray parameters= info.getJSONArray("parameters");
            JSONObject parameter= parameters.getJSONObject(0);
            int iparam=0;
            int iele=0;
            for ( int i=0; i<record.length(); i++ ) {
                QDataSet field= record.slice(i);
                Units u= (Units)field.property(QDataSet.UNITS);
                if ( u==null ) u= Units.dimensionless;
                units[i]= u;
                if ( UnitsUtil.isTimeLocation(u) ) {
                    unitsFormatter[i]= true;
                    datumFormatter[i]= TimeDatumFormatter.DEFAULT;
                    quotes[i]= false;
                    if ( parameter.has("length") ) {
                        lengths[i]= parameter.getInt("length");
                    } else {
                        throw new IllegalStateException("isotime measurement needs length");
                    }
                    
                } else if ( UnitsUtil.isNominalMeasurement(u) ) {
                    unitsFormatter[i]= true;
                    datumFormatter[i]= new EnumerationDatumFormatter();
                    quotes[i]= true;
                    if ( parameter.has("length") ) {
                        lengths[i]= parameter.getInt("length");
                    } else {
                        throw new IllegalStateException("string measurement needs length");
                    }
                    
                } else {
                    String dfs= (String)field.property(QDataSet.FORMAT);
                    if ( dfs!=null && dfs.trim().length()>0 ) {
                        datumFormatter[i]= getDataFormatter( dfs, u );
                    } else {
                        datumFormatter[i]= u.getDatumFormatterFactory().defaultFormatter();
                    }
                    unitsFormatter[i]= false;
                    quotes[i]= false;
                    fill[i]= parameter.getString("fill");
                }
                iele++;
                if ( iele==lens[iparam] ) {
                    iparam++;
                    iele=0;
                    if ( iparam==parameters.length() ) {
                        if ( i+1!=record.length() ) {
                            throw new IllegalStateException("things have gone wrong");
                        }
                    } else {
                        parameter= parameters.getJSONObject(iparam);
                    }
                }
            }
//            if ( false ) {
//                System.err.println("===");
//                for ( int i=0; i<record.length(); i++ ) {
//                    System.err.println( String.format( "%4d %s", i,datumFormatter[i] ) );
//                }
//                System.err.println("===");
//            }
        } catch (JSONException ex) {
            logger.log(Level.SEVERE, null, ex);
        }
        
    }

    
    @Override
    public void sendRecord(OutputStream out, QDataSet record) throws IOException {
        int n= record.length();
        for ( int i=0; i<record.length(); i++ ) {
            QDataSet field= record.slice(i);
            Datum fieldDatum;
            fieldDatum= DataSetUtil.asDatum(field);
            if ( quotes[i] ) out.write('"');
            if ( fieldDatum.isFill() ) {
                String f= fill[i];
                if ( f==null ) {
                    logger.log(Level.SEVERE, "fill is not defined for parameter formatted to column #{0}", i);
                    throw new IllegalStateException("fill is not defined for parameter formatted to column #" + i);
                }
                out.write( f.getBytes() );
            } else {
                String s=  datumFormatter[i].format( fieldDatum, units[i] );
                if ( quotes[i] ) {
                    s= s.replaceAll("\"", "\"\""); // See https://github.com/hapi-server/data-specification/issues/99
                }
                byte[] bytes= s.getBytes(CHARSET_UTF8);
                if ( lengths[i]>0 && bytes.length>lengths[i] ) {
                    bytes= Util.trimUTF8( bytes, lengths[i] );
                }
                out.write( bytes );
            }
            if ( quotes[i] ) out.write('"');
            if ( i<n-1 ) out.write(',');
        }
        out.write((byte)10);
    }
    
    public QDataSet initializeReader( JSONObject info, String record ) {
        
        String[] ss= Util.csvSplit( record, -1 );
        
        DDataSet result;
        SparseDataSetBuilder sdsb= new SparseDataSetBuilder(2);
        sdsb.setLength(ss.length);
        
        result= DDataSet.createRank1(ss.length);
        try {
            JSONArray parameters= info.getJSONArray("parameters");
            for ( int iparameter=0; iparameter<parameters.length(); iparameter++ ) {
                JSONObject parameter= parameters.getJSONObject(iparameter);
                String t= parameter.getString("type");
                Units u;
                if ( t.equals("isotime") ) {
                    u= Units.us2000;
                } else {
                    u= Units.lookupUnits( parameter.getString("units") );
                }
                try {
                    result.putValue(iparameter,u.parse(ss[iparameter]).doubleValue(u));
                    result.putProperty(t, iparameter, u);
                } catch (ParseException ex) {
                    logger.log(Level.SEVERE, null, ex);
                }
                sdsb.putProperty( QDataSet.UNITS, iparameter, u );
            }
        } catch (JSONException ex) {
            logger.log(Level.SEVERE, null, ex);
        }
        
        result.putProperty( QDataSet.BUNDLE_0, sdsb.getDataSet() );
        return result;
    }
    
    /**
     * read a record which has been formatted by this.
     * @param record the formatted record.
     * @param recordInfo example record which provides the units for parsing.
     * @return the parsed record.
     */
    public QDataSet readRecord( String record, QDataSet recordInfo ) {
        
        WritableDataSet result= DDataSet.copy(recordInfo);
        
        String[] ss= Util.csvSplit(record, -1);
                
        for ( int i=0; i<recordInfo.length(); i++ ) {
            Units u= (Units)recordInfo.slice(i).property(QDataSet.UNITS);
            try {
                result.putValue( i,u.parse(ss[i]).doubleValue(u) );
            } catch (ParseException ex) {
                logger.log(Level.SEVERE, null, ex);
            }
        }
        
        return result;
    }

    @Override
    public void finalize(OutputStream out) {
        
    }
    
    
    public static void main( String[] args ) throws JSONException {
        JSONObject info= new JSONObject("{\n" +
"    \"HAPI\": \"1.1\",\n" +
"    \"__infoVersion__\": \"1.0\",\n" +
"    \"cadence\": \"PT1M\",\n" +
"    \"createdAt\": \"2017-08-23T16:18Z\",\n" +
"    \"parameters\": [\n" +
"        {\n" +
"            \"fill\": null,\n" +
"            \"length\": 24,\n" +
"            \"name\": \"Time\",\n" +
"            \"type\": \"isotime\",\n" +
"            \"units\": \"UTC\"\n" +
"        },\n" +
"        {\n" +
"            \"description\": \"temperature in garage, car\",\n" +
"            \"fill\": \"-1e31\",\n" +
"            \"name\": \"Temperature\",\n" +
"            \"type\": \"double\",\n" +
"            \"units\": \"deg F\"\n" +
"        }\n" +
"    ],\n" +
"    \"sampleStartDate\": \"2017-08-22T00:00:00.000Z\",\n" +
"    \"sampleStopDate\": \"2017-08-23T00:00:00.000Z\",\n" +
"    \"startDate\": \"2012-01-09T00:00:00.000Z\",\n" +
"    \"status\": {\n" +
"        \"code\": 1200,\n" +
"        \"message\": \"OK request successful\"\n" +
"    },\n" +
"    \"stopDate\": \"2017-08-23T16:00:00.000Z\",\n" +
"    \"x_uri\": \"file:/home/jbf/public_html/1wire/data/$Y/$m/$d/0B000800408DD710.$Y$m$d.d2s\"\n" +
"}");
        CsvDataFormatter m= new CsvDataFormatter();
        QDataSet rec= m.initializeReader( info, "2017-08-22T22:22:03.000Z,7.318E01");
        QDataSet r1= m.readRecord( "2017-08-22T22:23:03.000Z,7.318E01", rec);
        System.err.println( r1.slice(0).toString() );
        System.err.println( r1.slice(1).toString() );
        
        r1= m.readRecord( "2017-08-22T23:23:03.000Z,7.428E01", rec);
        System.err.println( r1.slice(0).toString() );
        System.err.println( r1.slice(1).toString() );
        
    }
}