package uk.ac.starlink.ttools.filter;

import gnu.jel.CompilationException;
import gnu.jel.CompiledExpression;
import gnu.jel.Library;
import java.io.IOException;
import java.util.Iterator;
import java.util.function.LongSupplier;
import uk.ac.starlink.table.RowAccess;
import uk.ac.starlink.table.RowSequence;
import uk.ac.starlink.table.RowSplittable;
import uk.ac.starlink.table.SequentialRowSplittable;
import uk.ac.starlink.table.StarTable;
import uk.ac.starlink.table.WrapperRowAccess;
import uk.ac.starlink.table.WrapperRowSequence;
import uk.ac.starlink.table.WrapperStarTable;
import uk.ac.starlink.ttools.jel.JELUtils;
import uk.ac.starlink.ttools.jel.DummyJELRowReader;
import uk.ac.starlink.ttools.jel.RandomJELRowReader;
import uk.ac.starlink.ttools.jel.SequentialJELRowReader;

/**
 * Filter for making algebraic (JEL) assertions about table data contents.
 *
 * @author   Mark Taylor
 * @since    2 May 2006
 */
public class AssertFilter extends BasicFilter {

    /**
     * Constructor.
     */
    public AssertFilter() {
        super( "assert", "<test-expr> [<msg-expr>]" );
    }

    protected String[] getDescriptionLines() {
        return new String[] {
            "<p>Check that a boolean expression is true for each row.",
            "If the expression <code>&lt;test-expr&gt;</code> does not",
            "evaluate true for any row of the table, execution terminates",
            "with an error.",
            "As long as no error occurs, the output table is identical",
            "to the input one.",
            "</p>",
            "<p>If the <code>&lt;msg-expr&gt;</code> parameter is supplied,",
            "then on failure it will be evaluated and its value presented",
            "in the error message.",
            "</p>",
            "<p>The exception generated by an assertion violation is of class",
            "<code>" + AssertException.class.getName() + "</code>",
            "although that is not usually obvious if you are running from",
            "the shell in the usual way.",
            "</p>",
            explainSyntax( new String[] { "test-expr", "msg-expr" } ),
        };
    }

    public ProcessingStep createStep( Iterator<String> argIt )
            throws ArgException {
        final String testExpr;
        final String msgExpr;
        if ( argIt.hasNext() ) {
            testExpr = argIt.next();
            argIt.remove();
        }
        else {
            throw new ArgException( "Missing test expression" );
        }
        if ( argIt.hasNext() ) {
            msgExpr = argIt.next();
            argIt.remove();
        }
        else {
            msgExpr = null;
        }
        return new ProcessingStep() {
            public StarTable wrap( StarTable base ) throws IOException {
                return new JELAssertionTable( base, testExpr, msgExpr );
            }
        };
    }

    /**
     * Utility method to compile an expression.
     * Any compilation exception is converted to an informative IOException.
     *
     * @param  lib  library
     * @param  table   table context
     * @param  expr   expression to compile, may be null
     * @return  compiled expression, or null for null <code>expr</code>
     * @throws  IOException in case of compilation failure
     */
    private static CompiledExpression compile( Library lib, StarTable table,
                                               String expr ) 
            throws IOException {
        if ( expr == null ) {
            return null;
        }
        else {
            try {
                return JELUtils.compile( lib, table, expr );
            }
            catch ( CompilationException e ) {
                throw JELUtils.toIOException( e, expr );
            }
        }
    }

    /**
     * Used to define message object evaluation.
     */
    @FunctionalInterface
    private interface MessageSupplier {

        /**
         * Supplies an object, but may throw any throwable.
         *
         * @return  message object, typically a String;
         *          if non-null its toString() method will
         *          provide the user-directed message
         */
        Object getMessage() throws Throwable;
    }

    /**
     * StarTable implementation which performs assertions.
     * Table behaviour is exactly the same as that of the base table,
     * except that an {@link AssertException} will be thrown if the assertion
     * is violated.
     */
    private static class JELAssertionTable extends WrapperStarTable {
        private final String expr_;
        private final String msgExpr_;
        private final StarTable baseTable_;
        private final RandomJELRowReader randomReader_;
        private final CompiledExpression compEx_;
        private final CompiledExpression msgCompEx_;
        private final boolean requiresRowIndex_;

        /**
         * Constructor.
         *
         * @param  baseTable  base input table
         * @param  expr  JEL expression to assert
         * @param  msgExpr  JEL expression for assertion failure message
         */
        public JELAssertionTable( StarTable baseTable, String expr,
                                  String msgExpr )
                throws IOException {
            super( baseTable );
            baseTable_ = baseTable;
            expr_ = expr;
            msgExpr_ = msgExpr;
            randomReader_ =
                RandomJELRowReader.createConcurrentReader( baseTable );
            Library lib = JELUtils.getLibrary( randomReader_ );
            compEx_ = compile( lib, baseTable, expr );
            msgCompEx_ = compile( lib, baseTable, msgExpr );
            requiresRowIndex_ = randomReader_.requiresRowIndex();
            try {
                JELUtils
               .checkExpressionType( lib, baseTable, expr, boolean.class );
            }
            catch ( CompilationException e ) {
                throw JELUtils.toIOException( e, expr );
            }
        }

        public Object getCell( long irow, int icol ) throws IOException {
            Object cell = super.getCell( irow, icol );
            assertAtRow( randomReader_, irow );
            return cell;
        }

        public Object[] getRow( long irow ) throws IOException {
            Object[] row = super.getRow( irow );
            assertAtRow( randomReader_, irow );
            return row;
        }

        private void assertAtRow( RandomJELRowReader reader, long irow )
                throws IOException {
            Object result;
            try {
                result = reader.evaluateAtRow( compEx_, irow );
            }
            catch ( IOException | RuntimeException | Error e ) {
                throw e;
            }
            catch ( Throwable e ) {
                throw (IOException) new IOException( e.getMessage() )
                                   .initCause( e );
            }
            MessageSupplier msgSupplier =
                () -> reader.evaluateAtRow( msgCompEx_, irow );
            check( result, irow, msgSupplier );
        }

        public RowAccess getRowAccess() throws IOException {
            final RowAccess baseAcc = super.getRowAccess();
            final RandomJELRowReader accReader =
                RandomJELRowReader.createAccessReader( baseTable_, baseAcc );
            Library lib = JELUtils.getLibrary( accReader );
            final CompiledExpression compEx = compile( lib, baseTable_, expr_ );
            return new WrapperRowAccess( baseAcc ) {
                public void setRowIndex( long irow ) throws IOException {
                    super.setRowIndex( irow );
                    assertAtRow( accReader, irow );
                }
            };
        }

        public RowSequence getRowSequence() throws IOException {
            final SequentialJELRowReader seqReader =
                new SequentialJELRowReader( baseTable_ );
            Library lib = JELUtils.getLibrary( seqReader );
            final CompiledExpression compEx = compile( lib, baseTable_, expr_ );
            CompiledExpression msgCompEx = compile( lib, baseTable_, msgExpr_ );
            final MessageSupplier msgSupplier =
                () -> seqReader.evaluate( msgCompEx );
            return new WrapperRowSequence( seqReader ) {
                long lrow_;
                public boolean next() throws IOException {
                    boolean next = super.next();
                    if ( next ) {
                        Object result;
                        try {
                            result = seqReader.evaluate( compEx );
                        }
                        catch ( IOException | RuntimeException | Error e ) {
                            throw e;
                        }
                        catch ( Throwable e ) {
                            throw (IOException)
                                  new IOException( e.getMessage() )
                                 .initCause( e );
                        }
                        check( result, lrow_++, msgSupplier );
                    }
                    return next;
                }
            };
        }

        public RowSplittable getRowSplittable() throws IOException {
            RowSplittable baseSplittable = baseTable.getRowSplittable();
            if ( requiresRowIndex_ && baseSplittable.rowIndex() == null ) {
                return new SequentialRowSplittable( this );
            }
            else {
                return new AssertRowSplittable( baseTable.getRowSplittable() );
            }
        }

        /**
         * Performs the actual assertion.
         *
         * @param  result  assertion value, Boolean.TRUE for success
         * @param  irow    row index
         * @param  msgSupplier   in case of failure (result not true)
         *                       it will be evaluated, and if evaluation is
         *                       successful and returns a non-null value,
         *                       that value will be communicated to the user
         */
        private void check( Object result, long irow,
                            MessageSupplier msgSupplier )
                throws AssertException {
            if ( ! ( result instanceof Boolean ) ||
                 ! ((Boolean) result).booleanValue() ) {
                StringBuffer sbuf = new StringBuffer();
                sbuf.append( "Assertion \"" )
                    .append( expr_ )
                    .append( "\" violated at row " )
                    .append( irow + 1 );
                if ( msgSupplier != null ) {
                    Object msg;
                    try {
                        msg = msgSupplier.getMessage();
                    }
                    catch ( Throwable e ) {
                        msg = null;
                    }
                    if ( msg != null ) {
                        sbuf.append( ": " )
                            .append( msg );
                    }
                }
                throw new AssertException( sbuf.toString() );
            }
        }

        /**
         * RowSplittable implementation for use with AssertFilter.
         */
        private class AssertRowSplittable extends WrapperRowSequence
                                          implements RowSplittable {
            final RowSplittable baseSplit_;
            final SequentialJELRowReader seqReader_;
            final CompiledExpression compEx1_;
            final LongSupplier rowIndex_;
            final MessageSupplier msgSupplier_;

            /**
             * Constructor.
             *
             * @param   baseSplit  base splittable
             */
            AssertRowSplittable( RowSplittable baseSplit ) throws IOException {
                super( baseSplit );
                baseSplit_ = baseSplit;
                seqReader_ = new SequentialJELRowReader( baseTable_, baseSplit);
                LongSupplier rowIndex = baseSplit.rowIndex();
                if ( rowIndex == null ) {
                    rowIndex_ = () -> Long.MIN_VALUE;
                    assert ! requiresRowIndex_;
                }
                else {
                    rowIndex_ = rowIndex;
                }
                Library lib = JELUtils.getLibrary( seqReader_ );
                compEx1_ = compile( lib, baseTable_, expr_ );
                final CompiledExpression msgCompEx1 =
                    compile( lib, baseTable_, msgExpr_ );
                msgSupplier_ = () -> seqReader_.evaluate( msgCompEx1 );
            }
            public long splittableSize() {
                return baseSplit_.splittableSize();
            }
            public LongSupplier rowIndex() {
                return baseSplit_.rowIndex();
            }
            @Override
            public boolean next() throws IOException {
                boolean next = super.next();
                if ( next ) {
                    Object result;
                    try {
                        result = seqReader_.evaluate( compEx1_ );
                    }
                    catch ( IOException | RuntimeException | Error e ) {
                        throw e;
                    }
                    catch ( Throwable e ) {
                        throw (IOException) new IOException( e.getMessage() )
                             .initCause( e );
                    }
                    check( result, rowIndex_.getAsLong(), msgSupplier_ );
                }
                return next;
            }
            @Override
            public AssertRowSplittable split() {
                RowSplittable split1 = baseSplit_.split();
                try {
                    return split1 == null ? null
                                          : new AssertRowSplittable( split1 );
                }
                catch ( IOException e ) {
                    return null;
                }
            }
        }
    }
}
