package org.autoplot.csv;

import com.csvreader.CsvWriter;
import java.io.File;
import java.io.FileWriter;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.autoplot.ascii.AsciiTableDataSourceFormat;
import org.autoplot.datasource.AbstractDataSourceFormat;
import org.das2.datum.Datum;
import org.das2.datum.Units;
import org.das2.datum.format.DatumFormatter;
import org.das2.util.monitor.ProgressMonitor;
import org.das2.qds.DataSetUtil;
import org.das2.qds.QDataSet;
import org.das2.qds.SemanticOps;
import org.autoplot.datasource.URISplit;
import org.das2.datum.EnumerationUnits;
import org.das2.datum.TimeParser;
import org.das2.datum.UnitsUtil;
import org.das2.datum.format.DefaultDatumFormatter;
import org.das2.datum.format.FormatStringFormatter;
import org.das2.datum.format.TimeDatumFormatter;
import org.das2.datum.format.TimeDatumFormatterFactory;
import org.das2.qds.ops.Ops;
import org.das2.util.LoggerManager;

/**
 * Format data to CSV (comma separated values) file.
 * @author jbf
 */
public class CsvDataSourceFormat extends AbstractDataSourceFormat {

    private static final Logger logger= LoggerManager.getLogger("apdss.ascii.csv");
     
        private DatumFormatter getTimeFormatter( ) {
        DatumFormatter timeFormatter;
        String tformat= getParam( "tformat", "ISO8601" );
        String ft= tformat.toLowerCase();
        String depend0Units= getParam( "depend0Units", "" );
        Units dep0units= null;
        
        if ( depend0Units.length()>0 ) {
            try {
                dep0units= Units.lookupTimeUnits(depend0Units);
            } catch (ParseException ex) {
                Logger.getLogger(AsciiTableDataSourceFormat.class.getName()).log(Level.SEVERE, null, ex);
            }

            final Units tu= dep0units;
            if ( ft.equals("iso8601") ) ft=null;
            final String sformat= ft;
            timeFormatter= new DefaultDatumFormatter() {
                @Override
                public String format(Datum datum) {
                    return format(datum, tu);
                }
                @Override
                public String format(Datum datum, Units units) {
                    if ( datum.isFill() ) {
                        return "fill";
                    } else {
                        if ( sformat!=null && sformat.startsWith("%") ) {
                            return String.format( sformat, datum.doubleValue(tu) );
                        } else {
                            return String.valueOf( datum.doubleValue(tu) );
                        }
                    }
                }
            };
        } else if (ft.equals("iso8601")) {
            timeFormatter = TimeDatumFormatterFactory.getInstance().defaultFormatter();
            
        } else if ( tformat.startsWith("%")
                || ft.startsWith("$") ) {
            if ( tformat.startsWith("$") ) { // provide convenient URI-friendly spec
                tformat= tformat.replaceAll("\\$", "%");
            }
            tformat= tformat.replaceAll("\\+",getParam("delim",","));
            try {
                timeFormatter = new TimeDatumFormatter(tformat);
            } catch (ParseException ex) {
                logger.log(Level.SEVERE, ex.getMessage(), ex);
                try {
                    timeFormatter = new TimeDatumFormatter("%Y-%m-%dT%H:%M:%S");
                } catch (ParseException ex1) {
                    throw new RuntimeException(ex1);
                }
            }
        } else {
            try {
                if (ft.equals("day")) {
                    timeFormatter = new TimeDatumFormatter("%Y-%m-%d");
                } else if (ft.equals("hour")) {
                    timeFormatter = new TimeDatumFormatter("%Y-%m-%dT%H:%MZ");
                } else if (ft.startsWith("min")) {
                    timeFormatter =  new TimeDatumFormatter("%Y-%m-%dT%H:%MZ");
                } else if (ft.startsWith("sec")) {
                    timeFormatter =  new TimeDatumFormatter("%Y-%m-%dT%H:%M:%SZ");
                } else if (ft.startsWith("millisec")) {
                    final TimeParser tp= TimeParser.create("$Y-$m-$dT$H:$M:$S.$(subsec,places=3)");
                    timeFormatter= new DatumFormatter() {
                        @Override
                        public String format(Datum datum) {
                            return tp.format(datum);
                        }
                    };
                    //timeFormatter =  new TimeDatumFormatter("%Y-%m-%dT%H:%M:%S.%{milli}Z");
                } else if (ft.startsWith("microsec")) {
                    final TimeParser tp= TimeParser.create("$Y-$m-$dT$H:$M:$S.$(subsec,places=6)");
                    timeFormatter= new DatumFormatter() {
                        @Override
                        public String format(Datum datum) {
                            return tp.format(datum);
                        }
                    };
                    //timeFormatter =  new TimeDatumFormatter("%Y-%m-%dT%H:%M:%S.%{milli}%{micro}Z");
                } else if (ft.startsWith("nanosec")) {
                    final TimeParser tp= TimeParser.create("$Y-$m-$dT$H:$M:$S.$(subsec,places=9)");
                    timeFormatter= new DatumFormatter() {
                        @Override
                        public String format(Datum datum) {
                            return tp.format(datum);
                        }
                    };
                    //timeFormatter =  new TimeDatumFormatter("%Y-%m-%dT%H:%M:%S.%{milli}%{micro}Z");
                } else {
                    logger.log(Level.FINE, "not implemented: {0}", ft);
                    timeFormatter = new TimeDatumFormatter("%Y-%m-%dT%H:%M:%S");
                }

            } catch (ParseException ex) {
                logger.log( Level.SEVERE, ex.getMessage(), ex);
                timeFormatter = TimeDatumFormatterFactory.getInstance().defaultFormatter();
                
            }
        }
        return timeFormatter;

    }
        
    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();
        }
    }
    
    @Override
    public void formatData(String uri, QDataSet data, ProgressMonitor mon) throws Exception {
        
        super.setUri(uri);
                
        URISplit split = URISplit.parse( uri );
        
        Map<String,String> params= URISplit.parseParams(split.params);
        
        char delim= ',';
        if ( params.containsKey("delim") ) {
            String sdelimiter= params.get("delim");
            if ( sdelimiter.equals("COMMA") ) sdelimiter= ",";
            if ( sdelimiter.equals("SEMICOLON") ) sdelimiter= ";";
            delim= sdelimiter.charAt(0);
        }
        
        super.maybeMkdirs();
        
        mon.setTaskSize( data.length() );
        mon.started();
        
        File outFile= new File( split.resourceUri );
        FileWriter fw= new FileWriter(outFile);
        CsvWriter writer= null;
        try {
            writer= new CsvWriter( fw, delim );
            
            writer.setForceQualifier(true);
            writer.setUseTextQualifier(true);  // force quotes on header

            String[] values;
            String[] labels;

            int col=0;

            QDataSet[] dss;
            QDataSet[] wdss;

            List<QDataSet> ldss= new ArrayList();
            List<QDataSet> lwdss= new ArrayList();
            if ( data.property(QDataSet.DEPEND_0)!=null ) {
                ldss.add( (QDataSet) data.property(QDataSet.DEPEND_0));
                lwdss.add( DataSetUtil.weightsDataSet((QDataSet) data.property(QDataSet.DEPEND_0) ) );
                col++;
            }
            switch (data.rank()) {
                case 1:
                    ldss.add(data);
                    lwdss.add(DataSetUtil.weightsDataSet(data));
                    col++;
                    break;
                case 2:
                    if ( SemanticOps.isBundle(data) ) {
                        for ( int k=0; k<data.length(0); k++ ) {
                            QDataSet d1= Ops.unbundle(data, k);
                            ldss.add( d1 );
                            lwdss.add(DataSetUtil.weightsDataSet(d1));
                        }
                    } else {
                        ldss.add(data); // spectrogram
                        lwdss.add(DataSetUtil.weightsDataSet(data));
                    }
                    col+= data.length(0);
                    break;
                default:
                    throw new IllegalArgumentException("rank limit, data must be rank 1 sequence or a rank 2 table of data");
            }
            dss= ldss.toArray( new QDataSet[ldss.size()] );
            wdss= lwdss.toArray( new QDataSet[lwdss.size()] );
            values= new String[col];
            labels= new String[col];

            //set the headers
            {
                col= 0;
                for ( int ids=0; ids<dss.length; ids++ ) {
                    String name= (String)dss[ids].property(QDataSet.LABEL);
                    if ( name==null ) {
                        name=  (String)dss[ids].property(QDataSet.NAME);
                    }
                    if ( name==null ) {
                        name= "data"+ids;
                    }
                    String sunits;
                    Units units= SemanticOps.getUnits(dss[ids]);
                    if ( UnitsUtil.isTimeLocation(units) ) {
                        sunits= "UTC";
                    } else if ( units.isConvertibleTo(Units.dimensionless) ) {
                        sunits= null;
                    } else {
                        sunits= String.valueOf(units);
                    }
                    if ( dss[ids].rank()==1 ) {
                        labels[col++]= sunits==null ? name : name+" ("+sunits+")";
                    } else {
                        QDataSet dep1= (QDataSet) dss[ids].property(QDataSet.DEPEND_1);
                        if (dep1!=null && dep1.rank()==1) {
                            Units dep1units= SemanticOps.getUnits(dep1);
                            for ( int j=0;j<dss[ids].length(0); j++ ) {
                                labels[col++]= dep1units.format( Datum.create( dep1.value(j), dep1units ) );
                            }
                        } else {
                            for ( int j=0;j<dss[ids].length(0); j++ ) {
                                labels[col++]= name+" " +j+" ("+sunits+")";
                            }
                        }
                    }
                }
            }

            writer.writeRecord(labels);

            writer.setForceQualifier(false);
            writer.setUseTextQualifier(true);

            QDataSet bundleDesc= (QDataSet) data.property(QDataSet.BUNDLE_1);
            
            DatumFormatter tf= getTimeFormatter( );
            
            String df= getParam( "format", "" );
            
            DatumFormatter[] formats= new DatumFormatter[dss.length];
            for ( int jj=0; jj<dss.length; jj++ ) {
                QDataSet dssjj=dss[jj];
                Units u= SemanticOps.getUnits(dssjj);
                Units uu_jj=u;
                formats[jj]= u.getDatumFormatterFactory().defaultFormatter();
                if ( !( uu_jj instanceof EnumerationUnits ) ) {
                    String ff= bundleDesc!=null ? (String) bundleDesc.property(QDataSet.FORMAT,jj) : null;
                    if ( df.equals("") ) {
                        if ( ff==null ) {
                            double d1= dssjj.rank()==1 ? dssjj.value(0) : dssjj.value(0,0);
                            formats[jj]= uu_jj.createDatum(d1).getFormatter();
                        } else {
                            formats[jj]= getDataFormatter( ff, uu_jj );
                        }
                    } else {
                        if ( UnitsUtil.isTimeLocation( uu_jj ) ) {
                            formats[jj]= tf;
                        } else {
                            if ( ff==null ) {
                                formats[jj]= getDataFormatter( df, uu_jj );
                            } else {
                                formats[jj]= getDataFormatter( ff, uu_jj ); //TODO: what is user wants to override format? 
                            }
                        }
                    }
                } else {
                    formats[jj]= uu_jj.createDatum( dssjj.rank()==1 ? dssjj.value(0) : dssjj.value(0,0) ).getFormatter();
                }                
            }

            for ( int i=0; i<data.length(); i++ ) {
                mon.setTaskProgress(i);
                col= 0;
                for ( int ids=0; ids<dss.length; ids++ ) {
                    Units u= SemanticOps.getUnits(dss[ids]);
                    if ( dss[ids].rank()==1 ) {
                        if ( wdss[ids].value(i)==0 ) {
                            values[col++]= "NaN";
                        } else {
                            values[col++]= formats[ids].format( u.createDatum( dss[ids].value(i) ), u );
                        }
                    } else {
                        for ( int j=0;j<dss[ids].length(0); j++ ) {
                            if ( wdss[ids].value(i,j)==0 ) {
                                values[col++]= "NaN";
                            } else {
                                values[col++]= formats[ids].format( u.createDatum( dss[ids].value(i,j) ), u );
                            }
                        }
                    }
                }
                writer.writeRecord(values);
            }
        } finally {
            if ( writer!=null ) writer.close();
            fw.close();
            mon.finished();
        }
    }

    @Override
    public boolean canFormat(QDataSet ds) {
        return ds.rank()==1 || ds.rank()==2;
    }

    @Override
    public String getDescription() {
        return "Comma Separated Values";
    }
}