package uk.ac.starlink.topcat.plot2;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.BitSet;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import uk.ac.starlink.table.Tables;
import uk.ac.starlink.topcat.RowSubset;
import uk.ac.starlink.topcat.TopcatModel;
import uk.ac.starlink.ttools.plot2.DataGeom;
import uk.ac.starlink.ttools.plot2.Equality;
import uk.ac.starlink.ttools.plot2.PlotUtil;
import uk.ac.starlink.ttools.plot2.SubCloud;
import uk.ac.starlink.ttools.plot2.data.DataSpec;
import uk.ac.starlink.ttools.plot2.data.DataStore;
import uk.ac.starlink.ttools.plot2.data.TupleSequence;

/**
 * Point cloud representation for coordinates in a single table.
 * A TableCloud may aggregate SubClouds which are the same except that
 * they may represent different row subsets (row masks).
 * Each reference to a given point (a given table row) will only appear
 * in a TableCloud once.
 * When iterating over the points, no guarantee is given
 * about the order in which they appear.
 *
 * @author   Mark Taylor
 * @since    27 Jan 2014
 */
public abstract class TableCloud {

    private final DataGeom geom_;
    private final TopcatModel tcModel_;
    private final int iPosCoord_;

    /**
     * Constructor.
     *
     * @param   geom   converts coords to data positions
     * @param   tcModel  table in which these points occur
     * @param   iPosCoord  start position in tuple for position coordinates
     */
    protected TableCloud( DataGeom geom, TopcatModel tcModel, int iPosCoord ) {
        geom_ = geom;
        tcModel_ = tcModel;
        iPosCoord_ = iPosCoord;
    }

    /**
     * Returns this point cloud's data geom.
     *
     * @return   data geom
     */
    public DataGeom getDataGeom() {
        return geom_;
    }
 
    /**
     * Returns the table in which this point cloud's points are found.
     *
     * @return  data source
     */
    public TopcatModel getTopcatModel() {
        return tcModel_;
    }

    /**
     * Returns the position in tuples at which position coordinates are found.
     *
     * @return   position coord index
     */
    public int getPosCoordIndex() {
        return iPosCoord_;
    }

    /**
     * Returns the number of times that read will be called on the dataStore
     * for created tuple sequences.  This is used for progress updates.
     * Note it is not (necessarily) the number of tuples in created
     * tuple sequences.
     *
     * @return   total number of calls to base dataStore tupleSequence.next()
     *           generated by the result of <code>createTupleSequence</code>
     */
    public abstract long getReadRowCount();

    /**
     * Returns an array of subsets indicating the union of subsets
     * represented by this cloud.
     *
     * @return  array of contributing subsets
     */
    public abstract RowSubset[] getRowSubsets();

    /**
     * Returns the available information about the user data specification
     * for the positional coordinates represented by this cloud.
     *
     * @param   jPosCoord  offset into position coordinates (0 is first)
     * @return  user data specification
     */
    public abstract GuiCoordContent getGuiCoordContent( int jPosCoord );

    /**
     * Returns a sequence of tuples that will iterate over this cloud's points.
     *
     * @param   dataStore  data storage
     * @return  iterator over data tuples
     */
    public abstract TupleSequence createTupleSequence( DataStore dataStore );

    /**
     * Returns a list of TableClouds from a given list of SubClouds.
     * This collects together subclouds corresponding to each set of
     * position coordinates.
     * The returned array taken together will contain each included
     * position only once (the union of included subsets).
     *
     * @param  subClouds  point clouds by subset
     * @return   aggregated point clouds
     */
    public static TableCloud[] createTableClouds( SubCloud[] subClouds ) {

        /* Map: prepare a map grouping subclouds by key. */
        Map<PosKey,Collection<SubCloud>> cloudMap =
            new LinkedHashMap<PosKey,Collection<SubCloud>>();
        for ( int ic = 0; ic < subClouds.length; ic++ ) {
            SubCloud subCloud = subClouds[ ic ];
            PosKey posKey = getPosKey( subCloud );
            if ( ! cloudMap.containsKey( posKey ) ) {
                cloudMap.put( posKey, new HashSet<SubCloud>() );
            }
            cloudMap.get( posKey ).add( subCloud );
        }

        /* Reduce: for each key represented, group its subclouds into a
         * single table point cloud. */
        List<TableCloud> tcloudList = new ArrayList<TableCloud>();
        for ( Collection<SubCloud> clist : cloudMap.values() ) {
            TableCloud tcloud = createTableCloud( clist );
            if ( tcloud != null ) {
                tcloudList.add( tcloud ); 
            }
        }
        return tcloudList.toArray( new TableCloud[ 0 ] );
    }

    /**
     * Groups a set of subclouds representing compatible sets of positions
     * into a single TableCloud.
     *
     * @param   subClouds   point clouds representing the same positions
     *                      but different subsets
     * @return   single point cloud for all input subclouds
     */
    private static TableCloud
            createTableCloud( Collection<SubCloud> subClouds ) {

        /* These values are the same for all presented subclouds. */
        SubCloud cloud0 = subClouds.iterator().next();
        DataGeom geom = cloud0.getDataGeom();
        TopcatModel tcModel =
            GuiDataSpec.getTopcatModel( cloud0.getDataSpec() );
        long nTableRow = tcModel.getDataModel().getRowCount();

        /* Work out which subclouds we need to aggregate together. */
        List<SubCloud> useList = new ArrayList<SubCloud>();
        for ( SubCloud subCloud : subClouds ) {
            GuiDataSpec guiDataSpec = (GuiDataSpec) subCloud.getDataSpec();
            long rowCount = guiDataSpec.getKnownRowCount();

            /* If one of the subclouds contains all the rows, we can just
             * use that and forget the others. */
            if ( rowCount == nTableRow ) {
                return new SingleTableCloud( subCloud, tcModel );
            }

            /* If any of the subclouds contains no rows, we can ignore it. */
            else if ( rowCount != 0 ) {
                useList.add( subCloud );
            }
        }
        int nuse = useList.size();

        /* No subclouds, no points. */
        if ( nuse == 0 ) {
            return null;
        }

        /* One subcloud, make a table cloud based on it. */
        else if ( nuse == 1 ) {
            return new SingleTableCloud( useList.get( 0 ), tcModel );
        }

        /* Multiple non-trivial subclouds, we have to make a table cloud
         * based on the union of them all.  Although their position coordinates
         * may be in different places (different getPosCoordIndex values),
         * they are (by the contract of this method) all referring to the
         * same positions, so we can use position values from any one
         * of them (e.g. the first) while combining the inclusion
         * masks of all of them. */
        else {
            GuiDataSpec[] dataSpecs = new GuiDataSpec[ nuse ];
            for ( int iu = 0; iu < nuse; iu++ ) {
                dataSpecs[ iu ] = (GuiDataSpec) useList.get( iu ).getDataSpec();
            }
            int iPosCoord0 = useList.get( 0 ).getPosCoordIndex();
            return new UnionTableCloud( geom, tcModel, iPosCoord0, dataSpecs );
        }
    }

    /**
     * Returns a key that identifies for a subcloud the position coordinates
     * it identifies.  This aggregates the table, the DataGeom, and
     * the columns giving the position coordinates.
     *
     * @param  subCloud  input cloud
     * @return   key which is the same for subclouds that can be aggregated
     *           into a tablecloud
     */
    @Equality
    private static PosKey getPosKey( SubCloud subCloud ) {
        DataSpec dataSpec = subCloud.getDataSpec();
        TopcatModel tcModel = GuiDataSpec.getTopcatModel( dataSpec );
        DataGeom geom = subCloud.getDataGeom();
        int iPosCoord = subCloud.getPosCoordIndex();
        int npc = geom.getPosCoords().length;
        Object[] posCoordIds = new Object[ npc ];
        for ( int ipc = 0; ipc < npc; ipc++ ) {
            posCoordIds[ ipc ] = dataSpec.getCoordId( iPosCoord + ipc );
        }
        return new PosKey( tcModel, geom, posCoordIds );
    }

    /**
     * Represents the characteristics of a SubCloud that, if equal, mean
     * they can be aggregated with others to form a TableCloud.
     * Essentially this is all the interesting information apart from
     * row mask (subset).
     */
    @Equality
    private static class PosKey {
        final TopcatModel tcModel_;
        final DataGeom geom_;
        final Object[] posCoordIds_;

        /**
         * Constructor.
         *
         * @param  tcModel  table
         * @param  geom   data geom
         * @param   posCoordIds  identifier for position coordinates
         */
        PosKey( TopcatModel tcModel, DataGeom geom, Object[] posCoordIds ) {
            tcModel_ = tcModel;
            geom_ = geom;
            posCoordIds_ = posCoordIds;
        }
        @Override
        public int hashCode() {
            int code = -20013;
            code = 23 * code + tcModel_.hashCode();
            code = 23 * code + geom_.hashCode();
            code = 23 * code + Arrays.hashCode( posCoordIds_ );
            return code;
        }
        @Override
        public boolean equals( Object o ) {
            if ( o instanceof PosKey ) {
                PosKey other = (PosKey) o;
                return this.tcModel_ == other.tcModel_
                    && this.geom_.equals( other.geom_ )
                    && Arrays.equals( this.posCoordIds_, other.posCoordIds_ );
            }
            else {
                return false;
            }
        }
    }

    /**
     * TableCloud implementation based on a single subcloud.
     */
    private static class SingleTableCloud extends TableCloud {
        private final GuiDataSpec dataSpec_;

        /**
         * Constructor.
         *
         * @param  subCloud  single subcloud
         * @param  tcModel   table
         */
        public SingleTableCloud( SubCloud subCloud, TopcatModel tcModel ) {
            super( subCloud.getDataGeom(), tcModel,
                   subCloud.getPosCoordIndex() );
            dataSpec_ = (GuiDataSpec) subCloud.getDataSpec();
        }

        public RowSubset[] getRowSubsets() {
            return new RowSubset[] { dataSpec_.getRowSubset() };
        }

        public GuiCoordContent getGuiCoordContent( int jPosCoord ) {
            return dataSpec_.getGuiCoordContent( jPosCoord );
        }

        public TupleSequence createTupleSequence( DataStore dataStore ) {
            return dataStore.getTupleSequence( dataSpec_ );
        }

        public long getReadRowCount() {
            return dataSpec_.getRowCount();
        }
    }

    /**
     * TableCloud implementation based on multiple subclouds with the same
     * geoms.
     */
    private static class UnionTableCloud extends TableCloud {
        private final int iPosCoord0_;
        private final GuiDataSpec[] dataSpecs_;
        private final int nrow_;

        /**
         * Constructor.
         *
         * @param  geom  common data geom
         * @param  tcModel   table
         * @param  iPosCoord0    position coordinate index relating to the
         *                       first dataSpec
         */
        public UnionTableCloud( DataGeom geom, TopcatModel tcModel,
                                int iPosCoord0, GuiDataSpec[] dataSpecs ) {
            super( geom, tcModel, iPosCoord0 );
            iPosCoord0_ = iPosCoord0;
            dataSpecs_ = dataSpecs;
            nrow_ = Tables
                   .checkedLongToInt( tcModel.getDataModel().getRowCount() );
        }

        public RowSubset[] getRowSubsets() {
            int nspec = dataSpecs_.length;
            RowSubset[] subsets = new RowSubset[ nspec ];
            for ( int is = 0; is < nspec; is++ ) {
                subsets[ is ] = dataSpecs_[ is ].getRowSubset();
            }
            return subsets;
        }

        public GuiCoordContent getGuiCoordContent( int jPosCoord ) {
            return dataSpecs_[ 0 ]
                  .getGuiCoordContent( iPosCoord0_ + jPosCoord );
        }

        public TupleSequence createTupleSequence( final DataStore dataStore ) {
            return new UnionTupleSequence( dataStore, dataSpecs_, nrow_ );
        }

        public long getReadRowCount() {
            long nr = 0;
            for ( int i = 0; i < dataSpecs_.length; i++ ) {
                nr += dataSpecs_[ i ].getRowCount();
            }
            return nr;
        }
    }

    /**
     * TupleSequence which iterates over the union of points in a group of
     * similar DataSpecs.  The DataSpecs are assumed to have the same
     * coordinate information, but different masks.
     */
    private static class UnionTupleSequence implements TupleSequence {
        private final DataStore dataStore_;
        private final Iterator<DataSpec> dataSpecIt_;
        private final BitSet mask_;
        private TupleSequence baseTseq_;
        private int rowIndex_;

        /**
         * Constructor.
         *
         * @param  dataStore  data storage object
         * @param  dataSpecs  similar data specifiers (same coords)
         * @param  nrow   total number of rows in data-bearing table
         */
        UnionTupleSequence( DataStore dataStore, DataSpec[] dataSpecs,
                            int nrow ) {
            dataStore_ = dataStore;
            dataSpecIt_ = Arrays.asList( dataSpecs ).iterator();
            mask_ = new BitSet( nrow );
            baseTseq_ = PlotUtil.EMPTY_TUPLE_SEQUENCE;
            rowIndex_ = -1;
        }

        public boolean next() {
            while ( ! skipNext( baseTseq_ ) ) {
                if ( dataSpecIt_.hasNext() ) {
                    baseTseq_ =
                        dataStore_.getTupleSequence( dataSpecIt_.next() );
                }
                else {
                    return false;
                }
            }
            return true;
        }

        /**
         * This is difficult to split, because we need to avoid dispensing
         * the same row from different upstream sequences, which requires
         * coordination between instances/threads.
         * For now just don't do it, which means certain operations will
         * not be done in parallel for multi-subset plots.
         * These do not include the actual plotting operations,
         * but are mostly interactive things like identifying
         * a row index point from a click.
         */
        public TupleSequence split() {
            return null;
        }

        public long splittableSize() {
            return -1L;
        }

        private boolean skipNext( TupleSequence tseq ) {
            while ( true ) {
                if ( ! tseq.next() ) {
                    return false;
                }
                rowIndex_ = Tables.checkedLongToInt( tseq.getRowIndex() );
                if ( ! mask_.get( rowIndex_ ) ) {
                    mask_.set( rowIndex_ );
                    return true;
                }
            }
        }

        public long getRowIndex() {
            return rowIndex_;
        }

        public boolean getBooleanValue( int icol ) {
            return baseTseq_.getBooleanValue( icol );
        }

        public int getIntValue( int icol ) {
            return baseTseq_.getIntValue( icol );
        }

        public long getLongValue( int icol ) {
            return baseTseq_.getLongValue( icol );
        }

        public double getDoubleValue( int icol ) {
            return baseTseq_.getDoubleValue( icol );
        }

        public Object getObjectValue( int icol ) {
            return baseTseq_.getObjectValue( icol );
        }
    }
}
