package photoflockr;

import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.ext.swing.SwingTextField;
import javafx.scene.Cursor;
import javafx.scene.CustomNode;
import javafx.scene.Group;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.Node;
import javafx.scene.paint.Color;
import javafx.scene.Scene;
import javafx.scene.transform.Transform;
import javafx.scene.text.Font;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import javafx.lang.FX;

import java.lang.Math;
import java.util.HashMap;
import java.util.Map;

package var flockr: PhotoFlockr;

package class PhotoFlockr extends CustomNode {
    var fullScreen: Boolean;
    var maxSpeed: Integer = 2*10;
    var maxForce: Integer = (0.05*100) as Integer;
    var separation: Integer = 75*10;
    package var model: Model;
    var height: Number;
    var width: Number;
    var mouseX: Number;
    var mouseY: Number;
    var boids: Boid[];
    package var BOID_COUNT = 15 on replace { createBoids(); };
    var radius: Integer = (10*75/2.0) as Integer;
    var spriteGroupVisible: Boolean = true;
    var tagMap: Map;
    var maxTags: Integer = 12;
    var tagSearchField: SwingTextField;
    var spriteGroup: Group;

    // A sequence of nodes displaying the fetched pictures.
    var sprites = bind for (b in boids) {
        Sprite {
            fullScreen: bind fullScreen with inverse
            screenHeight: height
            screenWidth: width
            heading: bind b.vel.heading2D
            boid: b
            clickAction: function(tag: String): Void {
                tagSearchField.text = tag;
                model.queryString = tag;
            }
        }
    }

    // Calculates force of cohesion b/w two picture nodes.
    function tagCohesion(boid1: Boid, boid2: Boid): Number {
        var tagCount: Number = (sizeof boid1.tags + sizeof boid2.tags);
        if (tagCount == 0) {
            return 1;
        }
        var map = tagMap.get(boid1.id) as Map;
        if (map == null) {
            map = new HashMap();
            tagMap.put(boid1.id, map);
        }
        var result = map.get(boid2.id) as Number;
        if (result != null) {
            return result;
        }
        var count = 0.0;
        for (t1 in boid1.tags where count < maxTags) {
            for (t2 in boid2.tags where count < maxTags) {
                if (t1 == t2) {
                    count++;
                }
            }
        }
        result =  Math.min(count, maxTags);
        map.put(boid2.id, result);
        return result;
    }

    // Triggers frame-accurate movement calculation of the pictures.
    var boidsAnimator: Timeline = Timeline {
        keyFrames: KeyFrame {
            time: 1s / 25
            action: function() {
                for (b in boids) {
                    b.run(boids,
                        function(b1: Boid, b2: Boid): Number {
                            var c: Number = tagCohesion(b1, b2);
                            return if (maxTags == 0) 1 else c/maxTags;
                        }
                    );
                }
            }
            canSkip: true
        }
        repeatCount: Timeline.INDEFINITE
    }

    init {
        javax.imageio.ImageIO.setUseCache(false);
        model = Model {
            tagMap: tagMap = new HashMap()
            maxCount: bind BOID_COUNT
        };

        spriteGroupVisible = false;
        createBoids();
        maxTags = 12;
        boidsAnimator.play();

        // To avoid display of initial frozen screen,
        // make boids visible after 1 second
        Timeline {
            keyFrames: at (1s) { spriteGroupVisible => true }
        }.play();
    }

    bound function getPhoto(i: Integer) {
        if (i < sizeof model.photos) {
            model.photos[i]
        } else {
            null
        }
    }

    function createBoids(): Void {
        if (model != null) {
            boids =
                for (i in [0 .. < BOID_COUNT]) {
                    Boid {
                        id: i
                        radius: bind radius/10.0
                        width: width;
                        height: height;
                        maxspeed: bind maxSpeed / 10.0
                        maxforce: bind maxForce / 100.0
                        photoModel:  bind getPhoto(i)
                        desiredseparation: bind separation / 10.0;
                    }
                }
        }
    }

    // The selected sprite should bubble up the hierarchy to overlap other sprites.
    package function reorderSprites() {
        var _sprites = [ sprites[s | not s.boid.selected], sprites[s | s.boid.selected] ];
        spriteGroup.content = _sprites;
    }

    override function create(): Node {
        // Aux text field. It's used to calculate the width of the tag string a user types.
        // If it exceeds the width of the scene, the font size of the string is reduced
        // respectively.
        var tf = new javax.swing.JTextField();

        // The width of the aux text field.
        var tfWidth: Number = 0;
        // The scale factor to reduce/restore the font size.
        var tsfScaleFactor: Number = bind (width - 20) / Math.max(tfWidth, width - 20);
  
        tagSearchField = SwingTextField {
            borderless: true
            selectOnFocus: false
            width: bind (width - 20) / tsfScaleFactor
            text: bind model.queryString with inverse
            background: Color.color(0, 0, 0, 0)
            foreground: Color.WHITE
            font: Font { name: "Helvetica", size: 75 }
            opacity: 0.8
            transforms : bind [ Transform.translate(0, 60),
                                Transform.scale(tsfScaleFactor, tsfScaleFactor),
                                Transform.translate(0, -25) ];
        }

        tf.setFont(tagSearchField.getJTextField().getFont());

        tagSearchField.getJComponent().addKeyListener(
            java.awt.event.KeyAdapter {
                override public function keyReleased(ke: java.awt.event.KeyEvent) {
                    tf.setText(tagSearchField.getJTextField().getText());
                    tfWidth = tf.getPreferredSize().width;
                    if (not tagSearchField.focused) {
                        tagSearchField.requestFocus();
                    }
                    if (fullScreen) {
                        for (b in boids) {
                            b.selected = false;
                        }
                    }
                }
        });

        return Group {
            content: [
                //
                // A sequence of nodes displaying fetched pictures.
                //
                spriteGroup = Group {
                    content: sprites
                    visible: bind spriteGroupVisible
                },
                //
                // A tag search field.
                //
                Group {
                    translateX: 10
                    translateY: 160
                    content: tagSearchField
                },
                //
                // A node rendering crosshair cursor.
                //
                CrossHair {
                    w: width
                    h: height
                    cx: bind mouseX
                    cy: bind mouseY
                },
                //
                // A "glass pane".
                //
                Rectangle {
                    cursor: Cursor.NONE
                    height: height
                    width: width
                    fill: Color.rgb(0, 0, 0, 0)
                    onMouseMoved: function(e) {
                        mouseX = e.x;
                        mouseY = e.y;
                    }
                    onMouseDragged: function(e) {
                        mouseX = e.x;
                        mouseY = e.y;
                    }
                    smooth: false
                },
                //
                // A close control.
                //
                Group {
                    translateX: width - 20
                    content: [
                        ImageView {
                            translateX: 5
                            translateY: 5
                            image: Image {
                                url: "{__DIR__}images/close.png"
                            }
                            cache: true
                        },
                        Rectangle {
                            width: 20
                            height: 20
                            fill: Color.rgb(0, 0, 0, 0)
                            cursor: Cursor.HAND
                            cache: true
                            onMouseClicked: function(e) {
                                FX.exit();
                            }
                        }
                    ]
                }
            ]
        }
    }
}

function run(__ARGS__: String[]) {
    flockr = PhotoFlockr {
        width: 500
        height: 500
    }
    
    Stage {
        title: "Photo Flockr - JavaFX"
        visible: true
        resizable: false
        style: StageStyle.UNDECORATED
        scene: Scene {
            fill: Color.BLACK
            content: flockr
        }
    }
}