package uk.ac.starlink.ttools.scheme;

import java.util.Random;
import java.util.function.UnaryOperator;
import java.util.stream.Stream;

/**
 * Defines an interface and some implementations for a family of
 * (potentially) strange attractors.
 *
 * @author   Mark Taylor
 * @since    21 Jul 2020
 */
public abstract class AttractorFamily {

    private final String name_;
    private final int ndim_;
    private final int nparam_;
    private final double maxAbsParam_;
    private final double fillThresh_;

    /**
     * Clifford attractors; 2d, with four parameters.
     *
     * @see  <a href="https://examples.pyviz.org/attractors/attractors.html"
     *               >https://examples.pyviz.org/attractors/attractors.html</a>
     */
    public static final AttractorFamily CLIFFORD = createClifford();

    /**
     * Rampe attractors; 3d, wit six parameters.
     *
     * @see <a href="https://softologyblog.wordpress.com/2009/10/19/3d-strange-attractors/"
     *              >https://softologyblog.wordpress.com/2009/10/19/3d-strange-attractors/</a>
     */
    public static final AttractorFamily RAMPE = createRampe();

    /**
     * Henon attractors; 2d, with three parameters (quite boring).
     *
     * @see  <a href="https://en.wikipedia.org/wiki/H%C3%A9non_map"
     *               >https://en.wikipedia.org/wiki/H%C3%A9non_map</a>
     */
    public static final AttractorFamily HENON = createHenon();

    /**
     * Constructor.
     *
     * @param  name  name of family
     * @param  ndim  dimensionality of output points
     * @param  nparam  number of numerical parameters required to define
     *                 an attractor
     * @param  maxAbsParam   the maximum absolute recommended value of each of
     *                       the numerical parameters
     * @param  fillThresh   guideline space filling factor above which
     *                      attractors in this family look interesting (strange)
     */
    protected AttractorFamily( String name, int ndim, int nparam,
                               double maxAbsParam, double fillThresh ) {
        name_ = name;
        ndim_ = ndim;
        nparam_ = nparam;
        maxAbsParam_ = maxAbsParam;
        fillThresh_ = fillThresh;
    }

    /**
     * Returns the name of this family.
     *
     * @return  name
     */
    public String getName() {
        return name_;
    }

    /**
     * Returns the dimensionality of points in the sequence generated by
     * attractors from this family.
     *
     * @return  output space dimensionality
     */
    public int getDimCount() {
        return ndim_;
    }

    /**
     * Returns the number of parameters required to produce an
     * attractor instance.
     *
     * @return   number of required numeric parameters
     */
    public int getParamCount() {
        return nparam_;
    }

    /**
     * The maximum absolute value recommended for each of the input values
     * (which should therefore be in the range [-maxAbsParam,+maxAbsParam]).
     *
     * @return  range for numeric parameters
     */
    public double getMaxAbsParam() {
        return maxAbsParam_;
    }

    /**
     * Returns a guideline value for the space filling factor which
     * represents an interesting (strange) attractor from this family.
     * Many or most attractors in a given family are quite boring
     * (not strange).
     * 
     * @return   approximate space fraction threshold for strangeness
     * @see   #getSpaceFraction
     */
    public double getFillThreshold() {
        return fillThresh_;
    }

    /**
     * Returns a list of lines giving a textual representation of
     * the iteration formula used to generate attractors.
     * The expectation is that this will be displayed in a fixed-width font,
     * with one element per display line.
     *
     * @return  textual representation of iteration equations
     */
    public abstract String[] getEquations();

    /**
     * Returns a URL on the WWW at which additional or background
     * information about this attractor family may be found.
     *
     * @return   documentation URL, or null if none known
     */
    public abstract String getDocUrl();

    /**
     * Returns the operator which produces the next ndim-element output point
     * from the previous one.
     *
     * @param   params   nparam-element list of numeric parameters defining
     *                   the attractor
     * @return   point generator function
     */
    public abstract UnaryOperator<double[]> createOperator( double[] params );

    /**
     * Creates an attractor in this family by supplying numeric parameters.
     *
     * @param  params   nparam-element array of numeric parameters
     * @param  seed     initial point for iteration, or null for default
     * @return  attractor
     */
    public Attractor createAttractor( double[] params, double[] seed ) {
        return new Attractor( this, params,
                              seed == null ? new double[ nparam_ ] : seed );
    }

    /**
     * Creates a random attractor in this family.
     *
     * @param  rnd  random seed
     * @return  attractor
     */
    public Attractor createAttractor( Random rnd ) {
        return new Attractor( this,
                              randoms( rnd, getParamCount(), maxAbsParam_ ),
                              new double[ nparam_ ] );
    }

    /**
     * Returns a hard coded list of attractor parameter arrays that represent
     * interesting members of this family.
     * Suitable examples can be generated by running
     * {@link #getStrangeAttractors}.
     * This list should contain at least one entry.
     *
     * @return  example parameter arrays for strange attractors
     */
    public abstract double[][] getExamples();

    /**
     * Returns a fixed size array containing random numbers suitable
     * for use as attractor parameters.
     * The values are reasonably "round", which means they can be written
     * down without a lot of significant figures.
     *
     * @param  rnd   random seed
     * @param  n     required size of output array
     * @param  absmax    absolute maximum value
     * @return  n-element array of values each in the range [-absmax,+absmax]
     */
    public static double[] randoms( Random rnd, int n, double absmax ) {
        double[] rnds = new double[ n ];
        for ( int i = 0; i < n; i++ ) {
            double r = 2 * absmax * rnd.nextDouble() - absmax;
            r = (int) ( r * 100 ) / 100.;
            rnds[ i ] = r;
        }
        return rnds;
    }

    /**
     * Calculates a measure of the proportion of the N-dimensional space
     * defined by the bounds of an attractor.  This is a useful value
     * for identifying "interesting" attractors; boring/non-strange ones
     * may collapse down to a small number of points or lines, but
     * ones which return a high value probably look nice.
     *
     * <p>Of course the actual proporition for a finite number of points
     * is effectively zero, but we bin the space up into a regular grid
     * and count how many of the grid elements are hit during iteration.
     *
     * @param  att   attractor to characterise
     * @param  nbin   number of bins along each axis
     * @return  proportion of bins that are covered by a sequence of
     *          iterations
     */
    public static double getSpaceFraction( Attractor att, int nbin ) {
        int ndim = att.getFamily().getDimCount();
        int np = (int) Math.pow( 2 * nbin, ndim );
        Bounds bounds =
            att.pointStream().skip( 100 ).limit( np )
           .collect( () -> new Bounds( ndim ),
                     Bounds::update, Bounds::combine );
        Grid grid =
            att.pointStream().skip( 100 ).limit( np )
           .collect( () -> new Grid( bounds, nbin ),
                     Grid::accept, Grid::combine );
        return grid.getFillFraction();
    }

    /**
     * Utility/example method to create a stream of strange attractors
     * in a given family.
     *
     * @param   family   attractor family
     * @return   stream of strange attractors
     */
    public static Stream<Attractor>
            getStrangeAttractors( AttractorFamily family ) {
        final int nbin = 100;
        Random rnd = new Random( -445297 );
        return Stream.generate( () ->
            family.createAttractor( randoms( rnd, family.getParamCount(),
                                             family.maxAbsParam_ ), null )
        ).filter( att -> getSpaceFraction( att, nbin ) > family.fillThresh_ );
    }

    /**
     * Creates the Clifford attractor family.
     *
     * @return  new family
     */
    private static AttractorFamily createClifford() {
        return new AttractorFamily( "clifford", 2, 4, 2., 0.3 ) {
            public String getDocUrl() {
                return "https://examples.pyviz.org/attractors/attractors.html";
            }
            public UnaryOperator<double[]> createOperator( double[] params ) {
                final double a = params[ 0 ];
                final double b = params[ 1 ];
                final double c = params[ 2 ];
                final double d = params[ 3 ];
                return xy -> {
                    double x = xy[ 0 ];
                    double y = xy[ 1 ];
                    return new double[] {
                        Math.sin( a * y ) + c * Math.cos( a * x ),
                        Math.sin( b * x ) + d * Math.cos( b * y ),
                    };
                };
            }
            public String[] getEquations() {
                return new String[] {
                    "x' = sin(a*y) + c * cos(a*x)",
                    "y' = sin(b*x) + d * cos(b*y)",
                };
            }
            public double[][] getExamples() {
                return new double[][] {
                    { -1.31, -1.34,  1.19, -1.32 },
                    {  1.32, -1.44, -1.70, -1.58 },
                    {  1.27, -1.35,  0.82,  1.80 },
                    { -1.90,  1.18, -1.21,  1.07 },
                    { -1.27, -1.28,  1.00, -1.26 },
                    {  1.80,  0.90, -1.80 , 0.80 },
                };
            }
        };
    }

    /**
     * Creates the Rampe1 attractor family.
     *
     * @return  new family
     */
    private static AttractorFamily createRampe() {
        return new AttractorFamily( "rampe", 3, 6, 2., 0.01 ) {
            public String getDocUrl() {
                return "https://softologyblog.wordpress.com/"
                     + "2009/10/19/3d-strange-attractors/";
            }
            public UnaryOperator<double[]> createOperator( double[] params ) {
                final double a = params[ 0 ];
                final double b = params[ 1 ];
                final double c = params[ 2 ];
                final double d = params[ 3 ];
                final double e = params[ 4 ];
                final double f = params[ 5 ];
                return xyz -> {
                    double x = xyz[ 0 ];
                    double y = xyz[ 1 ];
                    double z = xyz[ 2 ];
                    return new double[] {
                        x * z * Math.sin( a * x ) - Math.cos( b * y ),
                        y * x * Math.sin( c * y ) - Math.cos( d * z ),
                        z * y * Math.sin( e * z ) - Math.cos( f * x ),
                    };
                };
            }
            public String[] getEquations() {
                return new String[] {
                    "x' = x * z * sin(a*x) - cos(b*y)",
                    "y' = y * x * sin(c*y) - cos(d*z)",
                    "z' = z * y * sin(e*z) - cos(f*x)",
                };
            }
            public double[][] getExamples() {
                return new double[][] {
                    {  1.82, -1.06, -0.92,  0.23,  1.92, -0.98 },
                    { -1.81,  1.35, -0.85,  0.32,  1.68, -1.62 },
                    {  0.23, -1.77,  1.32, -1.44, -1.70, -1.58 },
                    { -0.30,  1.78, -0.87,  1.69,  1.42,  1.21 },
                    {  1.42, -1.98,  0.39,  1.32,  1.79, -0.37 },
                };
            }
        };
    }

    /**
     * Creates the Henon attractor family.
     *
     * @return  new family
     */
    private static AttractorFamily createHenon() {
        return new AttractorFamily( "henon", 2, 3, 2., 0.01 ) {
            public String getDocUrl() {
                return "https://en.wikipedia.org/wiki/H%C3%A9non_map";
            }
            public UnaryOperator<double[]> createOperator( double[] params ) {
                final double a = params[ 0 ];
                final double b = params[ 1 ];
                final double c = params[ 2 ];
                return xy -> {
                    double x = xy[ 0 ];
                    double y = xy[ 1 ];
                    return new double[] {
                        y + a + b * x * x,
                        c * x,
                    };
                };
            }
            public String[] getEquations() {
                return new String[] {
                    "x' = y + a + b*x*x",
                    "y' = c*x",
                };
            }
            public double[][] getExamples() {
                return new double[][] {
                    { -0.80,  1.85,  0.24 },
                    { -0.68,  1.64,  0.36 },
                    {  1.73,  0.29, -0.99 },
                    {  0.88, -0.90, -0.93 },
                    {  1.40, -1.13, -0.01 },
                };
            }
        };
    }

    /**
     * Defines an unbounded sequence of points, that may represent an attractor.
     */
    public static class Attractor {

        private final AttractorFamily family_;
        private final double[] params_;
        private final double[] seed_;
        private final UnaryOperator<double[]> func_;

        /**
         * Constructor.
         *
         * @param  family  attractor family
         * @param  params   array of parameters defining this attractor's
         *                  actual behaviour
         * @param  seed    initial point for iteration
         */
        public Attractor( AttractorFamily family,
                          double[] params, double[] seed ) {
            family_ = family;
            params_ = params;
            seed_ = seed;
            func_ = family.createOperator( params );
        }

        /**
         * Returns this attractor's family.
         *
         * @return  family
         */
        public AttractorFamily getFamily() {
            return family_;
        }

        /**
         * Returns the unbounded point stream defined by this attractor.
         *
         * @return  point stream
         */
        public Stream<double[]> pointStream() {
            return Stream.iterate( seed_, func_ );
        }

        @Override
        public String toString() {
            StringBuffer sbuf = new StringBuffer()
                .append( family_.getName() )
                .append( '(' );
            for ( int i = 0; i < params_.length; i++ ) {
                if ( i > 0 ) {
                    sbuf.append( ',' );
                }
                sbuf.append( params_[ i ] );
            }
            sbuf.append( ')' );
            return sbuf.toString();
        }
    }


    /**
     * Utility class representing an N-dimensional histogram.
     */
    private static class Grid {
        private final Bounds bounds_;
        private final int nbin_;
        private final int ndim_;
        private final double[] factors_;
        private int[] grid_;

        /**
         * Constructor.
         *
         * @param  bounds   bounds over which this object extends
         * @param  nbin   number of bins in each dimension
         */
        Grid( Bounds bounds, int nbin ) {
            bounds_ = bounds;
            nbin_ = nbin;
            ndim_ = bounds.ndim_;
            factors_ = new double[ ndim_ ];
            int npix = 1;
            for ( int i = 0; i < ndim_; i++ ) {
                factors_[ i ] = 0.99999999 * nbin
                              / ( bounds.maxs_[ i ] - bounds.mins_[ i ] );
                npix *= nbin;
            }
            grid_ = new int[ npix ];
        }

        /**
         * Updates histogram content with a point at a given position.
         *
         * @param  point  N-dimensional position vector
         */
        public void accept( double[] point ) {
            grid_[ toBinIndex( point ) ]++;
        }

        /**
         * Returns the proportion of bins that have been hit.
         *
         * @return  fill fraction
         */
        public double getFillFraction() {
            int nf = 0;
            for ( int i = 0; i < grid_.length; i++ ) {
                if ( grid_[ i ] > 0 ) {
                    nf++;
                }
            }
            return 1.0 * nf / grid_.length;
        }

        /**
         * Combines this grid with another one to form the union.
         *
         * @param  other  other grid
         * @return  union of grids
         */
        public Grid combine( Grid other ) {
            for ( int i = 0; i < grid_.length; i++ ) {
                grid_[ i ] += other.grid_[ i ];
            }
            return this;
        }

        /**
         * Gives the index into the bin array corresponding to a given
         * point in space.
         *
         * @param  point  N-dimensional position vector
         * @return  bin array index
         */
        private int toBinIndex( double[] point ) {
            int ibin = 0;
            int step = 1;
            for ( int i = 0; i < ndim_; i++ ) {
                ibin += step * toBinCoord( i, point[ i ] );
                step *= nbin_;
            }
            return ibin;
        }

        /**
         * Gives the quantised distance along an axis corresponding
         * to the bin slot.
         *
         * @param  idim  dimension index
         * @param  value  position on given axis
         * @return   bin index along axis
         */
        private int toBinCoord( int idim, double value ) {
            int ibin =
                (int) ( factors_[ idim ] * ( value - bounds_.mins_[ idim ] ) );
            return Math.min( nbin_ - 1, Math.max( 0, ibin ) );
        }
    }

    /**
     * Utility class representing an extensible region of N-dimensional space.
     */
    private static class Bounds {
        final int ndim_;
        double[] mins_;
        double[] maxs_;

        /**
         * Constructor.
         *
         * @param  ndim  dimensionality of space
         */
        Bounds( int ndim ) {
            ndim_ = ndim;
            mins_ = new double[ ndim ];
            maxs_ = new double[ ndim ];
            for ( int i = 0; i < ndim; i++ ) {
                mins_[ i ] = + Double.MAX_VALUE;
                maxs_[ i ] = - Double.MAX_VALUE;
            }
        }

        /**
         * Updates this bounds object to include a given point.
         *
         * @param  point   N-dimensional coordinate vector
         */
        void update( double[] point ) {
            for ( int i = 0; i < ndim_; i++ ) {
                mins_[ i ] = Math.min( mins_[ i ], point[ i ] );
                maxs_[ i ] = Math.max( maxs_[ i ], point[ i ] );
            }
        }

        /**
         * Combines with another bounds object to form a union.
         *
         * @param   other   other bounds
         * @return  union of bounds
         */
        Bounds combine( Bounds other ) {
            for ( int i = 0; i < ndim_; i++ ) {
                mins_[ i ] = Math.min( this.mins_[ i ], other.mins_[ i ] );
                maxs_[ i ] = Math.max( this.maxs_[ i ], other.maxs_[ i ] );
            }
            return this;
        }

        @Override
        public String toString() {
            StringBuffer sbuf = new StringBuffer();
            for ( int i = 0; i < ndim_; i++ ) {
                sbuf.append( (float) mins_[ i ] + " - " + (float) maxs_[ i ] )
                    .append( " ; " );
            }
            return sbuf.toString();
        }
    }
}
