package uk.ac.starlink.hapi;

import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Map;
import java.util.logging.Logger;
import uk.ac.starlink.table.RowSequence;
import uk.ac.starlink.table.StarTable;
import uk.ac.starlink.table.TableSink;
import uk.ac.starlink.util.IOConsumer;
import uk.ac.starlink.util.URLUtils;

/**
 * Knows how to load a HAPI table.
 *
 * @author   Mark Taylor
 * @since    23 Jan 2024
 */
public class HapiSource {

    private final HapiService service_;
    private final URL dataUrl_;
    private final HapiParam[] params_;
    private final String urlTxt_;
    private final String format_;
    private final URL standaloneUrl_;
    private final String label_;
    private static final Logger logger_ =
        Logger.getLogger( "uk.ac.starlink.hapi" );

    /**
     * Constructor.
     *
     * <p>The <code>params</code> parameter is optional, but if present
     * must match the data from the dataUrl.
     * If it is absent a query requesting the header data is submitted.
     *
     * @param  service  service
     * @param  dataUrl  URL of HAPI file without header
     * @param  params   parameters being read from dataUrl contents,
     *                  may be null
     */
    public HapiSource( HapiService service, URL dataUrl, HapiParam[] params ) {
        service_ = service;
        dataUrl_ = dataUrl;
        params_ = params;
        urlTxt_ = dataUrl.toString();
        Map<String,String> reqParams =
            HapiService.getRequestParameters( dataUrl );

        /* Determine data format. */
        format_ = reqParams.containsKey( "format" ) ? reqParams.get( "format" )
                                                    : "csv";

        /* Set up standalone URL. */
        if ( reqParams.containsKey( "include" ) ) {
            logger_.warning( "URL should not include 'include' param: "
                           + dataUrl );
        }
        try {
            standaloneUrl_ = URLUtils.newURL( dataUrl + "&include=header" );
        }
        catch ( MalformedURLException e ) {
            throw new AssertionError();
        }

        /* Come up with a human-readable label. */
        String dataset = null;
        String start = null;
        String stop = null;
        for ( HapiVersion version : HapiVersion.getStandardVersions() ) {
            if ( dataset == null ) {
                dataset = reqParams.get( version.getDatasetRequestParam() );
            }
            if ( start == null ) {
                start = reqParams.get( version.getStartRequestParam() );
            }
            if ( stop == null ) {
                stop = reqParams.get( version.getStopRequestParam() );
            }
        }
        label_ = ( dataset == null ? "HAPI" : dataset )
               + "-" + getTimeRangeText( start, stop );
    }

    /**
     * Returns a standalone URL from which it ought to be possible
     * to load this object's table.
     *
     * <p>Feeding the contents of the returned URL to
     * {@link HapiTableBuilder#streamStarTable} should generate a
     * table ready for use.
     * The table read by {@link #streamHapi} may not actually be
     * generated by this method but if the service obeys the HAPI standard
     * it ought to give the same results.
     *
     * @return  standalone HAPI format table load URL
     */
    public URL getStandaloneUrl() {
        return standaloneUrl_;
    }

    /**
     * Returns a human-readable label for the table that will be loaded.
     *
     * @return  table label
     */
    public String getLabel() {
        return label_;
    }

    /**
     * Streams this object's table to a supplied TableSink.
     *
     * @param  sink  table destination
     * @param  chunkLimit  maximum number of chunks allowed
     * @param  limitCallback  called with a message if chunk limit is exceeded;
     *                        may be null
     */
    public void streamHapi( TableSink sink, int chunkLimit,
                            IOConsumer<String> limitCallback )
            throws IOException {
        if ( params_ == null ) {
            try ( InputStream in =
                      service_.openChunkedStream( standaloneUrl_, chunkLimit,
                                                  limitCallback ) ) {
                new HapiTableBuilder().streamStarTable( in, sink, null );
            }
        }
        else {
            HapiTableReader rdr = new HapiTableReader( params_ );
            StarTable meta = rdr.createStarTable( null );
            try ( InputStream in =
                      service_.openChunkedStream( dataUrl_, chunkLimit,
                                                  limitCallback ) ) {
                sink.acceptMetadata( meta );
                RowSequence rseq =
                    rdr.createRowSequence( in, (Byte) null, format_ );
                while ( rseq.next() ) {
                    sink.acceptRow( rseq.getRow() );
                }
                sink.endRows();
            }
        }
    }

    @Override
    public int hashCode() {
        int code = 220139;
        code = ( 23 * code ) + urlTxt_.hashCode();
        code = ( 23 * code ) + service_.hashCode();
        return code;
    }

    @Override
    public boolean equals( Object o ) {
        if ( o instanceof HapiSource ) {
            HapiSource other = (HapiSource) o;
            return this.service_.equals( other.service_ )
                && this.urlTxt_.equals( other.urlTxt_ );
        }
        else {
            return false;
        }
    }

    /**
     * Returns a compact string giving an idea of the range between
     * two ISO-8601 times.
     *
     * @param  isoStart  start time in ISO-8601 format
     * @param  isoStop   stop time in ISO-8601 format
     * @return  compact string
     */
    private static String getTimeRangeText( String isoStart, String isoStop ) {
        double startSec = Times.isoToUnixSeconds( isoStart );
        double stopSec = Times.isoToUnixSeconds( isoStop );
        double sec = stopSec - startSec;
        if ( Double.isNaN( sec ) ) {
            return isoStart;
        }
        else {
            final int leng;
            final String unit;
            final String format;
            if ( sec < 60 ) {
                leng = (int) Math.round( sec );
                unit = "s";
                format = "yyyy-MM-dd'T'HH:mm:ss";
            }
            else if ( sec < 3600 ) {
                leng = (int) Math.round( sec / 60 );
                unit = "m";
                format = "yyyy-MM-dd'T'HH:mm";
            }
            else if ( sec < 3600 * 24 ) {
                leng = (int) Math.round( sec / 3600 );
                unit = "h";
                format = "yyyy-MM-dd'T'HH";
            }
            else if ( sec < 3600 * 24 * 100 ) {
                leng = (int) Math.round( sec / 3600 / 24 );
                unit = "d";
                format = "yyyy-MM-dd";
            }
            else if ( sec < 3600 * 24 * 365 * 2 ) {
                leng = (int) Math.round( sec / 3600 / 24 / 30 );
                unit = "M";
                format = "yyyy-MM";
            }
            else {
                leng = (int) Math.round( sec / 3600 / 24 / 365 );
                unit = "y";
                format ="yyyy";
            }
            return Times.formatUnixSeconds( (long) startSec, format )
                 + "+" + leng + unit;
        }
    }
}
