package icy.sequence;

import icy.image.IcyBufferedImage;
import icy.type.DataType;
import icy.type.collection.array.Array1DUtil;
import icy.type.collection.array.Array2DUtil;

/**
 * This class is intended for plugins that construct new sequences by:<br>
 *  - first, specifying a data-type and a 5D-size for the sequence to be created<br>
 *  - second, visiting each sample (x,y,z,t,c) to set its value.<br>
 * <br>
 * A typical example of such situation would be a plugin that take a sequence
 * in input and return a copy of that sequence in output.
 * 
 * For such plugins, the SequenceBuilder class provides some tools to ease the
 * creation of the output sequence, especially in a multi-thread context. To
 * cut a long story short, here is an example of how the above-mentioned copy
 * algorithm could be implemented with SequenceBuilder:<br>
 * <br>
 *  <pre>
 *   // Input sequence
 *   Sequence in = ...
 *   
 *   // Create a SequenceBuilder object, that will be in charge of allocating
 *   // and feeding the output sequence.
 *   SequenceBuilder builder = new SequenceBuilder
 *   (
 *     // The output sequence will have the same size and data-type than the input
 *     in.getSizeX(), in.getSizeY(), in.getSizeZ(), in.getSizeT(), in.getSizeC(),
 *     in.getDataType_()
 *   );
 *   
 *   // Start the building process.
 *   builder.beginUpdate();
 *   try
 *   {
 *     // Here, the copy can be multi-threaded. No synchronization or mutex-locking
 *     // is required on the side of the SequenceBuilder object.
 *     
 *     ...
 *     
 *     // Thread 1
 *     forall(int t,z,c in listOfXYPlanesToBeDoneByThread1)
 *     {
 *       // Retrieve the array that will hold the pixel values corresponding to
 *       // the XY-plane at coordinates (t, z, c) in the output sequence. If this
 *       // object does not exist, it is created. The object returned by the
 *       // method getData is actually an instance of byte[], short[], int[],
 *       // float[] or double[], depending on the data-type specified in the
 *       // SequenceBuilder constructor.
 *       // 
 *       // Remark: depending on the context, it may be more convenient to call
 *       // builder.getDataAsDouble, builder.getDataAsByte, etc...
 *       // 
 *       Object buffer = builder.getData(t, z, c);
 *       
 *       // Execute the copy. For example:
 *       { Array1DUtil.arrayToArray(in.getDataXY(t, z, c), buffer); }
 *       
 *       // Mark the array returned by the previous call to builder.getData(t, z, c)
 *       // as ready to be incorporated in the output sequence.
 *       // 
 *       // Remark: there should only be one call to the method validateData
 *       // per set of coordinates (t, z, c).
 *       //
 *       builder.validateData(t, z, c);
 *     }
 *     
 *     ...
 *     
 *     // Thread 2
 *     forall(int t,z,c in listOfXYPlanesToBeDoneByThread2) {
 *       // etc...
 *     }
 *     
 *     ...
 *     
 *   }
 *   
 *   // Finish the building process (each call to beginUpdate() must be followed
 *   // by a call to endUpdate()).
 *   finally {
 *     builder.endUpdate();
 *   }
 *   
 *   // Return the sequence that have been created by the SequenceBuilder object.
 *   return builder.getResult();
 * </pre>
 * 
 * @author Yoann Le Montagner
 */
public class SequenceBuilder
{
        private int               _sizeX    ;
        private int               _sizeY    ;
        private int               _sizeZ    ;
        private int               _sizeT    ;
        private int               _sizeC    ;
        private DataType          _dataType ;
        private Sequence          _result   ;
        private SequenceAllocator _allocator;
        
        /**
         * Allocate a new sequence that will have the given size, dataType and an empty name
         */
        public SequenceBuilder(int sizeX, int sizeY, int sizeZ, int sizeT, int sizeC, DataType dataType)
        {
                this(sizeX, sizeY, sizeZ, sizeT, sizeC, dataType, null);
        }
        
        /**
         * If non-null, the 'target' argument will be used to store the result of the
         * sequence building process, and no new sequence will be created.<br>
         * <br>
         * Two situations may occur:
         * <li>The sequence 'target' has the same size and data-type than specified by
         * the arguments passed to the SequenceBuilder object. In that case, no new
         * buffer/image allocation is performed, and the sequence is only modified
         * through calls to the method Sequence.getDataXY().
         * </li>
         * <li>    
         * Otherwise, the sequence 'target' will be completly cleared (through a
         * call to the method Sequence.removeAllImages()) when first calling the
         * method SequenceBuilder.beginUpdate().
         * </li> 
         */
        public SequenceBuilder(int sizeX, int sizeY, int sizeZ, int sizeT, int sizeC, DataType dataType, Sequence target)
        {
                _sizeX     = sizeX   ;
                _sizeY     = sizeY   ;
                _sizeZ     = sizeZ   ;
                _sizeT     = sizeT   ;
                _sizeC     = sizeC   ;
                _dataType  = dataType;
                _result    = target==null ? new Sequence() : target;
                _allocator = null;
        }
        
        /**
         * Size X of the sequence to be created
         */
        public int getSizeX()
        {
                return _sizeX;
        }

        /**
         * Size Y of the sequence to be created
         */
        public int getSizeY()
        {
                return _sizeY;
        }

        /**
         * Size Z of the sequence to be created
         */
        public int getSizeZ()
        {
                return _sizeZ;
        }

        /**
         * Size T of the sequence to be created
         */
        public int getSizeT()
        {
                return _sizeT;
        }

        /**
         * Size C of the sequence to be created
         */
        public int getSizeC()
        {
                return _sizeC;
        }
        
        /**
         * Data-type of the sequence to be created
         */
        public DataType getDataType()
        {
                return _dataType;
        }
        
        /**
         * Return the output sequence
         */
        public Sequence getResult()
        {
                return _result;
        }
        
        /**
         * Check if the sequence is pre-allocated, i.e. if it already has the proper
         * size and data-type that was specified by the argument passed to the
         * SequenceBuilder constructor 
         */
        public boolean isPreAllocated()
        {
                return
                        
                        // Match the size ...
                        _result.getSizeX()==_sizeX &&
                        _result.getSizeY()==_sizeY &&
                        _result.getSizeZ()==_sizeZ &&
                        _result.getSizeT()==_sizeT &&
                        _result.getSizeC()==_sizeC &&
                        
                        // ... and the data-type in the case of non-empty sequences
                        (_result.getDataType_()==_dataType
                                || _sizeX==0
                                || _sizeY==0
                                || _sizeZ==0
                                || _sizeT==0
                                || _sizeC==0
                        );
        }
        
        /**
         * Start building the sequence
         * 
         * @throws IllegalStateException if the object is already in an "updating" state
         */
        public void beginUpdate()
        {
                if(_allocator!=null) {
                        throw new IllegalStateException("The SequenceBuilder object is already in an update state.");
                }
                _result.beginUpdate();
                
                // If the sequence has already the requested size and data-type, it can simply
                // be modified in-place: there is no need for new buffer allocation.
                if(isPreAllocated()) {
                        _allocator = new PreAllocatedAllocator(_result);
                }
                
                // Otherwise, the sequence is cleared, and dynamically rebuilt.
                else {
                        _result.removeAllImages();
                        _allocator = new OnFlyAllocator(_sizeX, _sizeY, _sizeZ, _sizeT, _sizeC, _dataType, _result);
                }
        }
        
        /**
         * Finish building the sequence.<br>
         * Nothing happens if beginUpdate() has not been called previously
         */
        public void endUpdate()
        {
                if(_allocator==null) {
                        return;
                }
                _result.endUpdate();
                _allocator = null;
        }
        
        /**
         * Retrieve or allocate the buffer for the XY plane corresponding to the given (t,z,c) coordinates
         * @throws NullPointerException if the method beginUpdate() has not been called previously.
         */
        public double[] getDataAsDouble(int t, int z, int c)
        {
                return (double[])_allocator.getData(t, z, c);
        }
        
        /**
         * Retrieve or allocate the buffer for the XY plane corresponding to the given (t,z,c) coordinates
         * @throws NullPointerException if the method beginUpdate() has not been called previously.
         */
        public float[] getDataAsFloat(int t, int z, int c)
        {
                return (float[])_allocator.getData(t, z, c);
        }
        
        /**
         * Retrieve or allocate the buffer for the XY plane corresponding to the given (t,z,c) coordinates
         * @throws NullPointerException if the method beginUpdate() has not been called previously.
         */
        public byte[] getDataAsByte(int t, int z, int c)
        {
                return (byte[])_allocator.getData(t, z, c);
        }
        
        /**
         * Retrieve or allocate the buffer for the XY plane corresponding to the given (t,z,c) coordinates
         * @throws NullPointerException if the method beginUpdate() has not been called previously.
         */
        public short[] getDataAsShort(int t, int z, int c)
        {
                return (short[])_allocator.getData(t, z, c);
        }
        
        /**
         * Retrieve or allocate the buffer for the XY plane corresponding to the given (t,z,c) coordinates
         * @throws NullPointerException if the method beginUpdate() has not been called previously.
         */
        public int[] getDataAsInt(int t, int z, int c)
        {
                return (int[])_allocator.getData(t, z, c);
        }
        
        /**
         * Retrieve or allocate the buffer for the XY plane corresponding to the given (t,z,c) coordinates
         * @throws NullPointerException if the method beginUpdate() has not been called previously.
         */
        public Object getData(int t, int z, int c)
        {
                return _allocator.getData(t, z, c);
        }
        
        /**
         * Validate the buffer for the XY plane corresponding to the given (t,z,c) coordinates
         * @throws NullPointerException if the method beginUpdate() has not been called previously.
         */
        public void validateData(int t, int z, int c)
        {
                _allocator.validateData(t, z, c);
        }

        
        /**
         * Interface to access the data of the targeted sequence
         */
        private interface SequenceAllocator
        {
                /**
                 * Retrieve or allocate the buffer for the XY plane corresponding to the given (t,z,c) coordinates
                 */
                public Object getData(int t, int z, int c);
                
                /**
                 * Validate the buffer for the XY plane corresponding to the given (t,z,c) coordinates
                 */
                public void validateData(int t, int z, int c);
        }
        
        
        /**
         * Virtual allocator dedicated to pre-allocated sequences
         */
        private static class PreAllocatedAllocator implements SequenceAllocator
        {
                private Sequence _target;
                
                public PreAllocatedAllocator(Sequence target)
                {
                        _target = target;
                }
                
                @Override
                public Object getData(int t, int z, int c)
                {
                        return _target.getDataXY(t, z, c);
                }

                @Override
                public void validateData(int t, int z, int c)
                {
                        // Nothing to do
                }
        }
        
        
        /**
         * Allocator use for dynamically allocated sequences
         */
        private static class OnFlyAllocator implements SequenceAllocator
        {
                private int              _sizeZ;
                private ImageAllocator[] _image;
                
                public OnFlyAllocator(int sizeX, int sizeY, int sizeZ, int sizeT, int sizeC, DataType dataType, Sequence target)
                {
                        int offset = 0;
                        int sizeZT = sizeT*sizeZ;
                        _sizeZ = sizeZ;
                        _image = new ImageAllocator[sizeZT];
                        for(int t=0; t<sizeT; ++t) {
                                for(int z=0; z<sizeZ; ++z) {
                                        _image[offset] = new ImageAllocator(sizeX, sizeY, sizeC, dataType, target, t, z);
                                        ++offset;
                                }
                        }
                }

                @Override
                public Object getData(int t, int z, int c)
                {
                        return _image[z+_sizeZ*t].getData(c);
                }

                @Override
                public void validateData(int t, int z, int c)
                {
                        _image[z+_sizeZ*t].validateData(c);
                }
        }
        
        
        /**
         * Provide tools to build the several IcyBufferedImage objects that compose
         * the final sequence
         */
        private static class ImageAllocator
        {
                private int       _sizeX    ;
                private int       _sizeY    ;
                private DataType  _dataType ;
                private Sequence  _target   ;
                private int       _t        ;
                private int       _z        ;
                private boolean   _done     ;
                private boolean[] _available;
                private Object[]  _data     ;
                
                /**
                 * Constructor
                 */
                public ImageAllocator(int sizeX, int sizeY, int sizeC, DataType dataType, Sequence target, int t, int z)
                {
                        _sizeX     = sizeX   ;
                        _sizeY     = sizeY   ;
                        _dataType  = dataType;
                        _target    = target  ;
                        _t         = t       ;
                        _z         = z       ;
                        _done      = false;
                        _data      = Array2DUtil.createArray(_dataType, sizeC);
                        _available = new boolean[sizeC];
                        for(int c=0; c<sizeC; ++c) {
                                _available[c] = false;
                        }
                }
                
                /**
                 * Allocate the buffer corresponding to the given channel
                 */
                public Object getData(int c)
                {
                        if(_data[c]==null) {
                                _data[c] = Array1DUtil.createArray(_dataType, _sizeX*_sizeY);
                        }
                        return _data[c];
                }
                
                /**
                 * Validate the buffer corresponding to the given channel.<br>
                 * If the results for all the channels are available, try to create the
                 * final buffered image
                 */
                public void validateData(int c)
                {
                        // Mark the current channel as valid and available
                        _available[c] = true;
                        
                        // Check whether the results for all the channels are available
                        for(boolean b : _available) {
                                if(!b) {
                                        return;
                                }
                        }
                        
                        // Synchronization is needed here, as several threads may try to create the
                        // final image at the same time
                        synchronized (this)
                        {
                                // Nothing to do if the image has been finalized during the synchronization
                                if(_done) {
                                        return;
                                }
                                
                                // Create the buffered image, and update the sequence
                                IcyBufferedImage image = new IcyBufferedImage(_sizeX, _sizeY, _data);
                                synchronized (_target) {
                                        _target.setImage(_t, _z, image);
                                }
                                
                                // Flag the image allocator as finalized
                                _done = true;
                        }
                }
        }
}