import io.data2viz.color.*
import io.data2viz.geom.*
import io.data2viz.math.*
import io.data2viz.timer.*
import io.data2viz.force.*
import io.data2viz.viz.*
import io.data2viz.random.*
import kotlin.math.atan
import kotlin.math.abs
import kotlin.math.max
fun main() {
    
    // FEEL FREE TO EXPERIMENT BY CHANGING THESE VALUES ************************
    
    val frictionRate = 4.pct				// friction rate
    val defaultIntensity = 70.pct			// default fixed intensity (no intensity decay)
    
    val linkForceIterations = 20			// the more iterations = the more "rigid" the curtains
    val collisionForceStrength = 20.pct		// less strength = wind pass through the "curtains"
    
    val singleCurtainWidth = 30				// width of a curtain
    val curtainsNumber = 1					// # of curtains drawn
    
    val curtainsLength = 30					// length of a curtain
    val stitchSpace = 8.0					// size between nodes
    
    val windRadius = 100.0					// size of the "wind" circles for collision
    val windSpeed = 6						// speed of the "wind"
    val windAngle = 0.deg					// angle of the "wind"
    
    val gravityValue = 0.15.pct				// higher values = heavier "curtains"
    
    // *************************************************************************
    
    val curtainsWidth = curtainsNumber * singleCurtainWidth
    val totalStitches = curtainsWidth * curtainsLength
    
    val vizSize = 900.0
    val movement = Vector(windSpeed * windAngle.cos, windSpeed * windAngle.sin)
    val randPos = RandomDistribution.uniform(150.0, 600.0)
    
    // our domain object, storing a default starting position and if it is "fixed" or not
    data class Stitch(val position:Point, val fixed:Boolean = false)
    
    // creating the objects, only the top line is "fixed"
    val stitches = (0 until totalStitches).map { 
        val col = it % curtainsWidth
        val row = it / curtainsWidth
        Stitch(
            point(80.0 + (col * stitchSpace), 50.0 + (row * stitchSpace) - col), 
            it < curtainsWidth
        ) 
    }.toMutableList()
    
    // adding 3 more nodes to the simulation, these nodes are used to simulate the wind
    // these nodes will have a very different behavior
    stitches += (Stitch(point(0, 350), false))
    stitches += (Stitch(point(-450, 600), false))
    stitches += (Stitch(point(-200, 400), false))
    lateinit var viz:Viz
    
    // keeping a reference to our ForceLink, this will allow easy access to the Links
    lateinit var forceLinks:ForceLink<Stitch>
        
    val simulation = forceSimulation<Stitch> {
        friction = frictionRate
        intensity = defaultIntensity
        intensityDecay = 0.pct
        
        // if the Stitch is "fixed", we use its current position has a fixed one (node won't move)
        initForceNode = {
            position = domain.position
            fixedX = if (domain.fixed) domain.position.x else null
            fixedY = if (domain.fixed) domain.position.y else null
        }
        
        // the force that creates links between the nodes
        // each node is linked to the next one on the right and next one below
        forceLinks = forceLink {
            linkGet = { 
                val links = mutableListOf<Link<Stitch>>()
                val currentCol = index % singleCurtainWidth
                val wholeCol = index % curtainsWidth
        		val row = index / curtainsWidth
                
                // only "link" the stitches, not the 3 nodes used for simulating the wind
                if (index < totalStitches) {
                    // check if we had the right-next node
                	if (currentCol != (singleCurtainWidth - 1) && wholeCol < curtainsWidth - 1) {
                     	links += Link(this, nodes[index + 1], stitchSpace)   
                    }
                    // check if we had the bottom-next node
                    if (row < curtainsLength - 1) {
                      	links += Link(this, nodes[index + curtainsWidth], stitchSpace)  
                    } 
                }
                // return the list of links
                links
            }
            iterations = linkForceIterations
        }
        
        // create a collision force, only the 3 "wind" nodes have a radius
        forceCollision {
            radiusGet = { if (index < totalStitches) .0 else windRadius }
            strength = collisionForceStrength
            iterations = 1
        }
        
        // create a "gravity" force, only applies to the "stiches" not the "wind"
        forceY {
            yGet = { vizSize }
            strengthGet = { if (index < totalStitches) gravityValue else 0.pct }
        }
        
        domainObjects = stitches
    }
   
	// storing the visuals of links and wind particles
    val links = mutableListOf<LineNode>()
    val winds = mutableListOf<CircleNode>()
    
    viz = viz {
        size = size(vizSize, vizSize)
        (totalStitches .. totalStitches+2).forEach {
            winds += circle {
                radius = windRadius
                fill = Colors.Web.lightblue.withAlpha(60.pct)
                x = -600.0
                y = -600.0
            }
        }
        forceLinks.links.forEach {
            if (it.target.index - it.source.index != 1) {
                links += line {
                    strokeWidth = stitchSpace + 1
                }
            }
        }
        
        animation {
            
            // force move the "wind" particles
            (totalStitches .. totalStitches+2).forEach {
                val windNode = simulation.nodes[it]
            	windNode.position += movement
            	if (windNode.x > vizSize) {
                 	windNode.x = -50.0   
                    windNode.y = randPos()
                }
                
                // uncomment these 4 lines to visualize the "wind" particles
                /*winds[it - totalStitches].apply {
                    x = windNode.x
                    y = windNode.y
                }*/
            }
            
            // show the new coordinates of each links to visualize the wind effect
            var currentIndex = 0
            forceLinks.links.forEach { link ->
                if (!link.source.domain.fixed && !link.target.domain.fixed) {
                    if (link.target.index - link.source.index != 1) {
                        links[currentIndex].apply {
                            x1 = link.source.x
                            x2 = link.target.x
                            y1 = link.source.y
                            y2 = link.target.y
                            // uncomment these 2 lines to tint each link depending on its angle...
                            val angle = ((max((y2 - y1), (x2 - x1)) - stitchSpace) * 5 + 180).deg
                            strokeColor = Colors.hsl(angle, 100.pct, 40.pct)
                        }
                        currentIndex += 1
                    }
                }
            }
        }
    }
    viz.bindRendererOnNewCanvas()
}