/*-
 * #%L
 * Scenery-backed 3D visualization package for ImageJ.
 * %%
 * Copyright (C) 2016 - 2018 SciView developers.
 * %%
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 
 * 1. Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 * #L%
 */

package sc.iview.commands.demo;

import bdv.BigDataViewer;
import bdv.util.RandomAccessibleIntervalSource;
import bdv.viewer.SourceAndConverter;
import cleargl.GLVector;
import graphics.scenery.BoundingGrid;
import graphics.scenery.volumes.Volume;
import ij.gui.GenericDialog;
import ij.gui.NonBlockingGenericDialog;
import net.imglib2.Cursor;
import net.imglib2.RandomAccess;
import net.imglib2.RandomAccessibleInterval;
import net.imglib2.Sampler;
import net.imglib2.img.Img;
import net.imglib2.img.array.ArrayImgs;
import net.imglib2.type.numeric.integer.UnsignedByteType;
import org.joml.Vector3f;
import org.scijava.command.Command;
import org.scijava.command.CommandService;
import org.scijava.command.InteractiveCommand;
import org.scijava.event.EventHandler;
import org.scijava.plugin.Menu;
import org.scijava.plugin.Parameter;
import org.scijava.plugin.Plugin;
import org.scijava.widget.Button;
import org.scijava.widget.NumberWidget;
import sc.iview.SciView;
import sc.iview.event.NodeRemovedEvent;

import javax.swing.*;

import java.util.HashMap;

import static sc.iview.commands.MenuWeights.DEMO;
import static sc.iview.commands.MenuWeights.DEMO_GAME_OF_LIFE;

/**
 * Conway's Game of Life—in 3D!
 *
 * @author Curtis Rueden
 * @author Kyle Harrington
 */
@Plugin(type = Command.class, menuRoot = "SciView", //
        menu = { @Menu(label = "Demo", weight = DEMO), //
                 @Menu(label = "Game of Life 3D", weight = DEMO_GAME_OF_LIFE) })
public class GameOfLife3D implements Command {

    private static final int ALIVE = 255;
    private static final int DEAD = 16;

    private static final String SIX = "6-connected";
    private static final String EIGHTEEN = "18-connected";
    private static final String TWENTY_SIX = "26-connected";

    @Parameter
    private SciView sciView;

    @Parameter(label = "Starvation threshold", min = "0", max = "26", persist = false)
    private int starvation = 5;

    @Parameter(label = "Birth threshold", min = "0", max = "26", persist = false)
    private int birth = 6;

    @Parameter(label = "Suffocation threshold", min = "0", max = "26", persist = false)
    private int suffocation = 9;

    @Parameter(choices = { SIX, EIGHTEEN, TWENTY_SIX }, persist = false)
    private String connectedness = TWENTY_SIX;

    @Parameter(label = "Initial saturation % when randomizing", min = "1", max = "99", style = NumberWidget.SCROLL_BAR_STYLE, persist = false)
    private int saturation = 10;

//    @Parameter(label = "Play speed", min = "1", max="100", style = NumberWidget.SCROLL_BAR_STYLE, persist = false)
    private int playSpeed = 10;
//
//    @Parameter(callback = "iterate")
//    private Button iterate;
//
//    @Parameter(callback = "randomize")
//    private Button randomize;
//
//    @Parameter(callback = "play")
//    private Button play;
//
//    @Parameter(callback = "pause")
//    private Button pause;

    private int w = 64, h = 64, d = 64;
    private Img<UnsignedByteType> field;
    private String name;
    private float[] voxelDims;
    private Volume volume;

    /** Temporary buffer for use while recomputing the image. */
    private boolean[] bits = new boolean[w * h * d];
    private GenericDialog dialog;

    /** Repeatedly iterates the simulation until stopped **/
    public void play() {
        sciView.animate( playSpeed, this::iterate);
    }

    /** Stops the simulation **/
    public void pause() {
        sciView.stopAnimation();
    }

    /** Randomizes a new bit field. */
    public void randomize() {
        final Cursor<UnsignedByteType> cursor = field.localizingCursor();
        final double chance = saturation / 100d;
        while( cursor.hasNext() ) {
            final boolean alive = Math.random() <= chance;
            cursor.next().set( alive ? ALIVE : DEAD );
        }
        updateVolume();
    }

    /** Performs one iteration of the game. */
    public void iterate() {
        final int connected;
        switch( connectedness ) {
        case SIX: connected = 6; break;
        case EIGHTEEN: connected = 18; break;
        default: connected = 26; break;
        }

        // compute the new image field
        final RandomAccess<UnsignedByteType> access = field.randomAccess();

        //RandomAccess<UnsignedByteType> access = ((SourceAndConverter) ((Volume.VolumeDataSource.RAIISource) volume.getDataSource()).getSources().get(0)).getSpimSource().getSource(0, 0).randomAccess();

        for( int z = 0; z < d; z++ ) {
            for( int y = 0; y < h; y++ ) {
                for( int x = 0; x < w; x++ ) {
                    final int i = z * w * h + y * w + x;
                    final int n = neighbors( access, x, y, z, connected );
                    access.setPosition( x, 0 );
                    access.setPosition( y, 1 );
                    access.setPosition( y, 2 );
                    if( alive( access ) ) {
                        // Living cell stays alive within (starvation, suffocation).
                        bits[i] = n > starvation && n < suffocation;
                    } else {
                        // New cell forms within [birth, suffocation).
                        bits[i] = n >= birth && n < suffocation;
                    }
                }
            }
        }

        // write the new bit field into the image
        final Cursor<UnsignedByteType> cursor = field.localizingCursor();
        while( cursor.hasNext() ) {
            cursor.fwd();
            final int x = cursor.getIntPosition( 0 );
            final int y = cursor.getIntPosition( 1 );
            final int z = cursor.getIntPosition( 2 );
            final boolean alive = bits[z * w * h + y * w + x];
            cursor.get().set( alive ? ALIVE : DEAD );
        }

//        for( int z = 0; z < d; z++ ) {
//            for( int y = 0; y < h; y++ ) {
//                for( int x = 0; x < w; x++ ) {
//                    access.setPosition( x, 0 );
//                    access.setPosition( y, 1 );
//                    access.setPosition( y, 2 );
//                    final boolean alive = bits[z * w * h + y * w + x];
//                    access.get().set( alive ? ALIVE : DEAD );
//                }
//            }
//        }

        updateVolume();
    }

    @Override
    public void run() {
        field = ArrayImgs.unsignedBytes( w, h, d );
        randomize();

        dialog = new GenericDialog("Game of Life 3D");
        dialog.addNumericField("Starvation threshold", starvation, 0);
        dialog.addNumericField("Birth threshold", birth, 0);
        dialog.addNumericField("Suffocation threshold", suffocation, 0);
        dialog.addNumericField("Initial saturation % when randomizing", saturation, 0);
        dialog.showDialog();

        if( dialog.wasCanceled() ) return;

        starvation = (int) dialog.getNextNumber();
        birth = (int) dialog.getNextNumber();
        suffocation = (int) dialog.getNextNumber();
        saturation = (int) dialog.getNextNumber();

        randomize();
        play();

//
//    @Parameter(callback = "iterate")
//    private Button iterate;
//
//    @Parameter(callback = "randomize")
//    private Button randomize;
//
//    @Parameter(callback = "play")
//    private Button play;
//
//    @Parameter(callback = "pause")
//    private Button pause;

        //play();

        //eventService.subscribe(this);
    }


    // -- Helper methods --

    private int neighbors( RandomAccess<UnsignedByteType> access, int x, int y, int z, int connected ) {
        int n = 0;
        // six-connected
        n += val( access, x - 1, y, z );
        n += val( access, x + 1, y, z );
        n += val( access, x, y - 1, z );
        n += val( access, x, y + 1, z );
        n += val( access, x, y, z - 1 );
        n += val( access, x, y, z + 1 );
        // eighteen-connected
        if( connected >= 18 ) {
            n += val( access, x - 1, y - 1, z );
            n += val( access, x + 1, y - 1, z );
            n += val( access, x - 1, y + 1, z );
            n += val( access, x + 1, y + 1, z );
            n += val( access, x - 1, y, z - 1 );
            n += val( access, x + 1, y, z - 1 );
            n += val( access, x - 1, y, z + 1 );
            n += val( access, x + 1, y, z + 1 );
            n += val( access, x, y - 1, z - 1 );
            n += val( access, x, y + 1, z - 1 );
            n += val( access, x, y - 1, z + 1 );
            n += val( access, x, y + 1, z + 1 );
        }
        // twenty-six-connected
        if( connected == 26 ) {
            n += val( access, x - 1, y - 1, z - 1 );
            n += val( access, x + 1, y - 1, z - 1 );
            n += val( access, x - 1, y + 1, z - 1 );
            n += val( access, x + 1, y + 1, z - 1 );
            n += val( access, x - 1, y - 1, z + 1 );
            n += val( access, x + 1, y - 1, z + 1 );
            n += val( access, x - 1, y + 1, z + 1 );
            n += val( access, x + 1, y + 1, z + 1 );
        }
        return n;
    }



    private int val( RandomAccess<UnsignedByteType> access, int x, int y, int z ) {
        if( x < 0 || x >= w || y < 0 || y >= h || z < 0 || z >= d ) return 0;
        access.setPosition( x, 0 );
        access.setPosition( y, 1 );
        access.setPosition( z, 2 );
        return alive( access ) ? 1 : 0;
    }

    private boolean alive( final Sampler<UnsignedByteType> access ) {
        return access.get().get() == ALIVE;
    }

    private long tick;

    private void updateVolume() {
        if( volume == null ) {
            name = "Life Simulation";
            voxelDims = new float[] { 1, 1, 1 };
            volume = ( Volume ) sciView.addVolume( field, name, voxelDims );

            BoundingGrid bg = new BoundingGrid();
            bg.setNode( volume );

//            volume.setVoxelSizeX(10.0f);
//            volume.setVoxelSizeY(10.0f);
//            volume.setVoxelSizeZ(10.0f);

            volume.putAbove(new Vector3f(0.0f, 0.0f, 0.0f));
//            volume.setRenderingMethod(2);
            volume.getTransferFunction().addControlPoint(0.0f, 0.0f);
            volume.getTransferFunction().addControlPoint(0.4f, 0.3f);

            volume.setName( "Game of Life 3D" );

            sciView.centerOnNode(volume);
        } else {
            // NB: Name must be unique each time.
            sciView.updateVolume( field, name + "-" + ++tick, voxelDims, volume );

//            RandomAccessibleIntervalSource<UnsignedByteType> newSource = new RandomAccessibleIntervalSource<UnsignedByteType>(field, new UnsignedByteType(), name + "-" + ++tick);
//
//            SourceAndConverter<UnsignedByteType> sourceAndConverter = BigDataViewer.wrapWithTransformedSource(
//                    new SourceAndConverter<>(newSource, BigDataViewer.createConverterToARGB(new UnsignedByteType())));
//
//            ((Volume.VolumeDataSource.RAISource) volume.getDataSource()).getSources().set(0, sourceAndConverter);
//
//            volume.getVolumeManager().notifyUpdate(volume);
//
//            volume.setDirty(true);
//            volume.setNeedsUpdate(true);
//            volume.getVolumeManager().requestRepaint();
            //volume.getCacheControl().prepareNextFrame();
        }
    }

    /**
     * Stops the animation when the volume node is removed.
     * @param event
     */
    @EventHandler
    private void  onNodeRemoved(NodeRemovedEvent event) {
        if(event.getNode() == volume) {
            sciView.stopAnimation();
        }
    }

    /**
     * Returns the current Img
     */
    public Img<UnsignedByteType> getImg() {
        return field;
    }

    /**
     * Returns the scenery volume node.
     */
    public Volume getVolume() {
        return volume;
    }

    public static void main(String... args) throws Exception {
        SciView sv = SciView.create();

        CommandService command = sv.getScijavaContext().getService(CommandService.class);

        HashMap<String, Object> argmap = new HashMap<>();

        command.run(GameOfLife3D.class, true, argmap);
    }
}