/*
 * Copyright (C) 2015 The 8-Bit Bunch. Licensed under the Apache License, Version 1.1
 * (the "License"); you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at <http://www.apache.org/licenses/LICENSE-1.1>.
 * Unless required by applicable law or agreed to in writing, software distributed under
 * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
 * ANY KIND, either express or implied. See the License for the specific language
 * governing permissions and limitations under the License.
 */
package org.badvision.outlaweditor;

import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;

import org.badvision.outlaweditor.api.ApplicationState;
import org.badvision.outlaweditor.data.TileMap;
import org.badvision.outlaweditor.data.TileUtils;
import org.badvision.outlaweditor.data.xml.Map;
import org.badvision.outlaweditor.data.xml.Script;
import org.badvision.outlaweditor.data.xml.Scripts;
import org.badvision.outlaweditor.data.xml.Tile;
import org.badvision.outlaweditor.ui.TileSelectModal;
import org.badvision.outlaweditor.ui.ToolType;

import jakarta.xml.bind.JAXBException;
import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.event.EventHandler;
import javafx.scene.Cursor;
import javafx.scene.Group;
import javafx.scene.ImageCursor;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.effect.DropShadow;
import javafx.scene.effect.Glow;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.image.WritableImage;
import javafx.scene.input.Clipboard;
import javafx.scene.input.DataFormat;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.ScrollEvent;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;

/**
 *
 * @author brobert
 */
public class MapEditor extends Editor<Map, MapEditor.DrawMode> implements EventHandler<MouseEvent> {

    Pane anchorPane;
    Canvas drawCanvas;
    private Tile currentTile;
    int posX = 0;
    int posY = 0;
    double zoom = 1.0;
    DrawMode drawMode = DrawMode.Pencil1px;
    TileMap currentMap;
    double tileWidth = getCurrentPlatform().tileRenderer.getWidth() * zoom;
    double tileHeight = getCurrentPlatform().tileRenderer.getHeight() * zoom;
    Color cursorAssistColor = new Color(0.2, 0.2, 1.0, 0.4);
    Script highlightedScript = null;

    @Override
    protected void onEntityUpdated() {
        currentMap = new TileMap(getEntity());
    }

    public TileMap getCurrentMap() {
        return currentMap;
    }

    EventHandler<ScrollEvent> scrollHandler = (ScrollEvent t) -> {
        if (t.isShiftDown()) {
            t.consume();
            if (t.getDeltaY() > 0) {
                zoomIn();
            } else {
                zoomOut();
            }
        }
    };

    public DrawMode getDrawMode() {
        return drawMode;
    }

    @Override
    public void setDrawMode(DrawMode drawMode) {
        this.drawMode = drawMode;
        switch (drawMode) {
            case TileEraser:
                ImageCursor cursor = new ImageCursor(new Image(MapEditor.class.getResourceAsStream("/images/eraser.png")));
                drawCanvas.setCursor(cursor);
                break;
            case Select:
                drawCanvas.setCursor(Cursor.CROSSHAIR);
                break;
            case ScriptPencil:
                drawCanvas.setCursor(Cursor.CLOSED_HAND);
                break;
            case ScriptEraser:
                drawCanvas.setCursor(Cursor.OPEN_HAND);
                break;
            default:
                setCurrentTile(getCurrentTile());
                break;
        }
    }

    @Override
    public void showShiftUI() {
        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
    }

    public void moveMapByY(int amt) {
        getCurrentMap().shift(0, amt);
        redraw();
    }

    public void moveMapByX(int amt) {
        getCurrentMap().shift(amt, 0);
        redraw();
    }

    @Override
    public void buildEditorUI(Pane tileEditorAnchorPane) {
        anchorPane = tileEditorAnchorPane;
        initCanvas();
        redraw();
    }

    @Override
    public void showSelectorModal() {
        String category = null;
        if (currentTile != null) {
            category = currentTile.getCategory();
        }
        if (this.equals(ApplicationState.getInstance().getController().getVisibleEditor())) {
            TileSelectModal.showTileSelectModal(anchorPane, category, this::setCurrentTile);
        }
    }

    public void initCanvas() {
        if (drawCanvas != null) {
            anchorPane.getChildren().remove(drawCanvas);
        }
        drawCanvas = new Canvas();
        drawCanvas.heightProperty().bind(ApplicationState.getInstance().getPrimaryStage().heightProperty().subtract(120));
        drawCanvas.widthProperty().bind(ApplicationState.getInstance().getPrimaryStage().widthProperty().subtract(200));
//        drawCanvas.widthProperty().bind(anchorPane.widthProperty());
        drawCanvas.widthProperty().addListener((ObservableValue<? extends Number> ov, Number t, Number t1) -> {
            redraw();
        });
        drawCanvas.heightProperty().addListener((ObservableValue<? extends Number> ov, Number t, Number t1) -> {
            redraw();
        });
        drawCanvas.addEventFilter(ScrollEvent.ANY, scrollHandler);
        drawCanvas.setOnMousePressed(this);
        drawCanvas.setOnMouseMoved(this);
        drawCanvas.setOnMouseDragged(this);
        drawCanvas.setOnMouseDragReleased(this);
        drawCanvas.setOnMouseReleased(this);
        anchorPane.getChildren().add(0, drawCanvas);
        cursorAssistant = new Rectangle(tileWidth, tileHeight, cursorAssistColor);
        cursorAssistant.setMouseTransparent(true);
        cursorAssistant.setEffect(new Glow(1.0));
        anchorPane.getChildren().add(cursorAssistant);
    }

    @Override
    public void addScript(Script script) {
        if (getCurrentMap().getBackingMap().getScripts() == null) {
            getCurrentMap().getBackingMap().setScripts(new Scripts());
        }
        getCurrentMap().getBackingMap().getScripts().getScript().add(script);
    }

    Script selectedScript = null;

    public void setSelectedScript(Script script) {
        selectedScript = script;
        highlightedScript = script;
    }

    public Script getSelectedScript() {
        return selectedScript;
    }

    private void drawScript(double x, double y, Script script) {
        if (script != null) {
            getCurrentMap().putLocationScript((int) x, (int) y, script);
        } else {
            getCurrentMap().removeLocationScripts((int) x, (int) y);
        }
        redraw();
    }

    public void assignScript(Script script, double x, double y) {
        int xx = (int) (x / tileWidth) + posX;
        int yy = (int) (y / tileHeight) + posY;
        getCurrentMap().putLocationScript(xx, yy, script);
        redraw();
    }

    public void unassignScripts(double x, double y) {
        int xx = (int) (x / tileWidth) + posX;
        int yy = (int) (y / tileHeight) + posY;
        getCurrentMap().removeLocationScripts(xx, yy);
        redraw();
    }

    public void clearScriptTriggers(Script s) {
        getCurrentMap().clearScriptTriggersFromMap(s);
        redraw();
    }

    public void togglePanZoom() {
        anchorPane.getChildren().stream().filter((n) -> !(n == drawCanvas)).forEach((n) -> {
            n.setVisible(!n.isVisible());
        });
    }

    public void scrollBy(int deltaX, int deltaY) {
        posX += deltaX * (drawCanvas.getWidth() / tileWidth / 2);
        posY += deltaY * (drawCanvas.getHeight() / tileWidth / 2);
        posX = Math.max(0, posX);
        posY = Math.max(0, posY);
        redraw();
    }

    public void zoomOut() {
        if (zoom <= 1) {
            zoom(-0.1);
        } else {
            zoom(-0.25);
        }
    }

    public void zoomIn() {
        if (zoom >= 1) {
            zoom(0.25);
        } else {
            zoom(0.1);
        }
    }

    private void zoom(double delta) {
        zoom += delta;
        zoom = Math.min(Math.max(0.15, zoom), 4.0);
        tileWidth = getCurrentPlatform().tileRenderer.getWidth() * zoom;
        tileHeight = getCurrentPlatform().tileRenderer.getHeight() * zoom;
        fullRedraw.set(true);
        redraw();
    }

    @Override
    public void observedObjectChanged(Map object) {
        redraw();
    }
    private AtomicBoolean fullRedraw = new AtomicBoolean(true);
    private long redrawRequested;
    private Thread redrawThread;

    @Override
    public void redraw() {
        redrawRequested = System.nanoTime();
        if (redrawThread == null || !redrawThread.isAlive()) {
            redrawThread = new Thread(() -> {
                long test = 0;
                while (test != redrawRequested) {
                    test = redrawRequested;
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException ex) {
                        Logger.getLogger(MapEditor.class.getName()).log(Level.SEVERE, null, ex);
                    }
                }
                Platform.runLater(() -> {
                    doRedraw(fullRedraw.getAndSet(false));
                });
                redrawThread = null;
            });
            redrawThread.start();
        }
    }

    private synchronized void doRedraw(boolean fullRedraw) {
        prepareCanvas(fullRedraw);
        int cols = (int) (drawCanvas.getWidth() / tileWidth);
        int rows = (int) (drawCanvas.getHeight() / tileHeight);
        for (int x = 0; x <= cols; x++) {
            for (int y = 0; y <= rows; y++) {
                Tile tile = currentMap.get(posX + x, posY + y);
                String id = (tile == null) ? null : tile.getId();
                boolean notSimilar = (id == null) != (tiles[x][y] == null);
                notSimilar |= (id != null && !id.equals(tiles[x][y]));
                if (notSimilar) {
                    tiles[x][y] = id;
                    doDraw(x, y, tile);
                }
            }
        }
        for (int x = 0; x <= cols; x++) {
            for (int y = 0; y <= rows; y++) {
                if (highlightScripts(x, y, currentMap.getLocationScripts(posX + x, posY + y), highlightedScript)) {
                    tiles[x][y] = "SCRIPT";
                }
            }
        }
        // Reposition arrows
        anchorPane.getChildren().get(1).setLayoutX((drawCanvas.getWidth() - 30) / 2);
        anchorPane.getChildren().get(2).setLayoutY((drawCanvas.getHeight() - 30) / 2);
        anchorPane.getChildren().get(3).setLayoutX((drawCanvas.getWidth() - 30) / 2);
        anchorPane.getChildren().get(4).setLayoutY((drawCanvas.getHeight() - 30) / 2);
    }

    String[][] tiles = null;

    private void prepareCanvas(boolean forceReset) {
        int cols = (int) (drawCanvas.getWidth() / tileWidth);
        int rows = (int) (drawCanvas.getHeight() / tileHeight);

        boolean reset = tiles == null || tiles.length <= cols || tiles[0] == null || tiles[0].length <= rows;
        if (forceReset || reset) {
            tiles = new String[cols + 1][rows + 1];
            fillEmpty(0, 0, drawCanvas.getWidth(), drawCanvas.getHeight());
        }
    }

    private Paint getFillPattern(double x, double y) {
        boolean oddEven = (x % 20) < 10;
        oddEven ^= (y % 20) < 10;
        return oddEven ? Color.BLACK : Color.NAVY;
    }

    private void doDraw(int x, int y, Tile tile) {
        double xx = x * tileWidth;
        double yy = y * tileHeight;
        if (tile != null) {
            drawCanvas.getGraphicsContext2D().drawImage(TileUtils.getImage(tile, getCurrentPlatform()), xx, yy, tileWidth, tileHeight);
        } else {
            fillEmpty(xx, yy, xx + tileWidth, yy + tileHeight);
        }
    }

    int patternSize = 10;

    private void fillEmpty(double startX, double startY, double endX, double endY) {
        for (double x = startX; x < endX; x = x - (x % patternSize) + patternSize) {
            for (double y = startY; y < endY; y = y - (y % patternSize) + patternSize) {
                double width = Math.min(patternSize, endX - x);
                double height = Math.min(patternSize, endY - y);
                drawCanvas.getGraphicsContext2D().setFill(getFillPattern(x, y));
                drawCanvas.getGraphicsContext2D().fillRect(x, y, width, height);
            }
        }
    }

    private static final int DASH_LENGTH = 3;

    private final Set<Script> invisibleScripts = new HashSet<>();

    public boolean isScriptVisible(Script script) {
        return !invisibleScripts.contains(script);
    }

    public void setScriptVisible(Script script, boolean visible) {
        setScriptVisible(script, visible, true);
    }

    public void setScriptVisible(Script script, boolean visible, boolean redraw) {
        if (visible) {
            invisibleScripts.remove(script);
        } else {
            invisibleScripts.add(script);
        }
        if (redraw) {
            redraw();
        }
    }

    private boolean highlightScripts(int x, int y, List<Script> scripts, Script extraHighlight) {
        if (scripts == null || scripts.isEmpty()) {
            return false;
        }
        List<Script> visibleScripts = scripts.stream().filter(this::isScriptVisible).collect(Collectors.toList());
        if (visibleScripts.isEmpty()) {
            return false;
        }
        GraphicsContext gc = drawCanvas.getGraphicsContext2D();
        if (highlightedScript != null && visibleScripts.contains(extraHighlight)) {
            Color color = currentMap.getScriptColor(extraHighlight).get().brighter();
            color = Color.color(color.getRed(), color.getGreen(), color.getBlue(), 0.5);
            gc.setFill(color);
            double xx = x * tileWidth;
            double yy = y * tileHeight;
            gc.fillRect(xx, yy, tileWidth, tileHeight);
        }
        int idx = 0;
        double xx = x * tileWidth;
        double yy = y * tileHeight;
        gc.setLineWidth(4);
        for (int i = 0; i < tileWidth - 2; i += DASH_LENGTH) {
            idx = (idx + 1) % visibleScripts.size();
            gc.beginPath();
            gc.moveTo(xx, yy);
            currentMap.getScriptColor(visibleScripts.get(idx)).ifPresent(gc::setStroke);
            xx += DASH_LENGTH;
            gc.lineTo(xx, yy);
            gc.setEffect(new DropShadow(2, Color.BLACK));
            gc.stroke();
        }
        for (int i = 0; i < tileHeight - 2; i += DASH_LENGTH) {
            idx = (idx + 1) % visibleScripts.size();
            gc.beginPath();
            gc.moveTo(xx, yy);
            currentMap.getScriptColor(visibleScripts.get(idx)).ifPresent(gc::setStroke);
            yy += DASH_LENGTH;
            gc.lineTo(xx, yy);
            gc.setEffect(new DropShadow(2, Color.BLACK));
            gc.stroke();
        }
        for (int i = 0; i < tileWidth - 2; i += DASH_LENGTH) {
            idx = (idx + 1) % visibleScripts.size();
            gc.beginPath();
            gc.moveTo(xx, yy);
            currentMap.getScriptColor(visibleScripts.get(idx)).ifPresent(gc::setStroke);
            xx -= DASH_LENGTH;
            gc.lineTo(xx, yy);
            gc.setEffect(new DropShadow(2, Color.BLACK));
            gc.stroke();
        }
        for (int i = 0; i < tileHeight - 2; i += DASH_LENGTH) {
            idx = (idx + 1) % visibleScripts.size();
            gc.beginPath();
            gc.moveTo(xx, yy);
            currentMap.getScriptColor(visibleScripts.get(idx)).ifPresent(gc::setStroke);
            yy -= DASH_LENGTH;
            gc.lineTo(xx, yy);
            gc.setEffect(new DropShadow(2, Color.BLACK));
            gc.stroke();
        }
        return true;
    }

    public void setupDragDrop(TransferHelper<Script> scriptHelper, TransferHelper<ToolType> toolHelper) {
        scriptHelper.registerDropSupport(drawCanvas, (Script script, double x, double y) -> {
            assignScript(script, x, y);
        });
        toolHelper.registerDropSupport(drawCanvas, (ToolType tool, double x, double y) -> {
            unassignScripts(x, y);
        });

    }

    @Override
    public void unregister() {
        drawCanvas.widthProperty().unbind();
        drawCanvas.heightProperty().unbind();
        anchorPane.getChildren().remove(drawCanvas);
        currentMap.updateBackingMap();
    }

    /**
     * @return the currentTile
     */
    public Tile getCurrentTile() {
        return currentTile;
    }

    /**
     * @param currentTile the currentTile to set
     * @return Same tile (necessary for callback support)
     */
    public Tile setCurrentTile(Tile currentTile) {
        if (drawMode == DrawMode.TileEraser) {
            drawMode = DrawMode.Pencil1px;
        }
        this.currentTile = currentTile;
        ImageCursor cursor = new ImageCursor(TileUtils.getImage(currentTile, getCurrentPlatform()), 2, 2);
        drawCanvas.setCursor(cursor);
        return currentTile;
    }

    public void showPreview() {
        byte[] data = getCurrentPlatform().imageRenderer.renderPreview(currentMap, posX, posY, getCurrentPlatform().maxImageWidth, getCurrentPlatform().maxImageHeight);
        WritableImage img = getCurrentPlatform().imageRenderer.renderImage(null, data, getCurrentPlatform().maxImageWidth, getCurrentPlatform().maxImageHeight);
        Stage stage = new Stage();
        stage.setTitle("Preview");
        ImageView imgView = new ImageView(img);
        Group root = new Group(imgView);
        stage.setScene(new Scene(root, img.getWidth(), img.getHeight()));
        stage.show();
    }

    public void copyScript(Script s) {
        java.util.Map<DataFormat, Object> clip = new HashMap<>();
        clip.put(DataFormat.PLAIN_TEXT, "selection/map/" +
            ApplicationState.getInstance().getGameData().getMap().indexOf(getEntity()) +
            "/script/" + getEntity().getScripts().getScript().indexOf(s));
        Clipboard.getSystemClipboard().setContent(clip);
    }

    @Override
    public void copy() {
        byte[] data = getCurrentPlatform().imageRenderer.renderPreview(currentMap, posX, posY, getCurrentPlatform().maxImageWidth, getCurrentPlatform().maxImageHeight);
        WritableImage img = getCurrentPlatform().imageRenderer.renderImage(null, data, getCurrentPlatform().maxImageWidth, getCurrentPlatform().maxImageHeight);
        java.util.Map<DataFormat, Object> clip = new HashMap<>();
        clip.put(DataFormat.IMAGE, img);
        if (drawMode != DrawMode.Select || startX >= 0) {
            clip.put(DataFormat.PLAIN_TEXT, "selection/map/" + ApplicationState.getInstance().getGameData().getMap().indexOf(getEntity()) + "/" + getSelectionInfo());
        }
        Clipboard.getSystemClipboard().setContent(clip);
        clearSelection();
    }

    @Override
    public String getSelectedAllInfo() {
        setSelectionArea(posX, posY, posX + 19, posY + 11);
        String result = getSelectionInfo();
        return result;
    }

    @Override
    public void paste() {
        if (Clipboard.getSystemClipboard().hasContent(DataFormat.PLAIN_TEXT)) {
            String clipboardInfo = (String) Clipboard.getSystemClipboard().getContent(DataFormat.PLAIN_TEXT);
            java.util.Map<String, Integer> selection = TransferHelper.getSelectionDetails(clipboardInfo);
            if (selection.containsKey("script")) {
                Map sourceMap = ApplicationState.getInstance().getGameData().getMap().get(selection.get("map"));
                Script sourceScript = sourceMap.getScripts().getScript().get(selection.get("script"));
                try {
                    Script cloneScript = TransferHelper.cloneObject(sourceScript, Script.class, "script");
                    if (sourceMap.equals(getEntity())) {
                        cloneScript.setName(cloneScript.getName() + " CLONE");
                    }
                    addScript(cloneScript);
                    ApplicationState.getInstance().getApplicationUI().redrawScripts();
                } catch (JAXBException ex) {
                    Logger.getLogger(MapEditor.class.getName()).log(Level.SEVERE, null, ex);
                }
            } else {
                if (selection.containsKey("map")) {
                    trackState();
                    Map sourceMap = ApplicationState.getInstance().getGameData().getMap().get(selection.get("map"));
                    TileMap source = getCurrentMap();
                    if (!sourceMap.equals(getCurrentMap().getBackingMap())) {
                        source = new TileMap(sourceMap);
                    } else {
                        source.updateBackingMap();
                    }
                    int height = selection.get("y2") - selection.get("y1");
                    int width = selection.get("x2") - selection.get("x1");
                    int x1 = selection.get("x1");
                    int y1 = selection.get("y1");
                    for (int y = 0; y < height; y++) {
                        for (int x = 0; x < width; x++) {
                            plot(x + lastX, y + lastY, source.get(x + x1, y + y1));
                        }
                    }
                }
            }
        }
    }

    @Override
    public void select() {
        setDrawMode(DrawMode.Select);
    }

    @Override
    public void selectNone() {
        clearSelection();
    }

    Rectangle cursorAssistant;

    private void updateCursorAssistant(MouseEvent t) {
        if (t.getEventType() == MouseEvent.MOUSE_EXITED) {
            cursorAssistant.setVisible(false);
        } else {
            cursorAssistant.setVisible(true);
            cursorAssistant.setWidth(tileWidth);
            cursorAssistant.setHeight(tileHeight);
            cursorAssistant.setTranslateX(t.getX() - (t.getX() % tileWidth));
            cursorAssistant.setTranslateY(t.getY() - (t.getY() % tileHeight));
        }
    }

    public void removeScript(Script script) {
        getCurrentMap().removeScriptFromMap(script);
        redraw();
    }

    private org.badvision.outlaweditor.api.Platform getCurrentPlatform() {
        return ApplicationState.getInstance().getCurrentPlatform();
    }

    @Override
    public void copyData() {
        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
    }

    private boolean hasScriptsAt(int x, int y) {
        List<Script> scripts = currentMap.getLocationScripts(x, y);
        return scripts != null && !scripts.isEmpty();
    }

    public static enum DrawMode {

        Pencil1px, Pencil3px, Pencil5px, FilledRect, TileEraser(false), ScriptPencil(false), Select(false), ScriptEraser(false);

        boolean requireTile = false;

        DrawMode() {
            this(true);
        }

        DrawMode(boolean require) {
            requireTile = require;
        }

        public boolean requiresCurrentTile() {
            return requireTile;
        }
    };

    public void plot(final int x, final int y, final Tile t) {
        if (x < 0) {
            return;
        }
        if (y < 0) {
            return;
        }
        if (x >= getEntity().getWidth()) {
            System.out.println("X out of bounds " + getEntity().getWidth());
            return;
        }
        if (y >= getEntity().getHeight()) {
            System.out.println("Y out of bounds " + getEntity().getHeight());
            return;
        }
        currentMap.put(x, y, t);
        doDraw(x - posX, y - posY, t);
    }

    public void drawBrush(int x, int y, int size, Tile t) {
        for (int xx = x - size; xx <= x + size; xx++) {
            for (int yy = y - size; yy <= y + size; yy++) {
                if (Math.sqrt((xx - x) * (xx - x) + (yy - y) * (yy - y)) > size) {
                    continue;
                }
                plot(xx, yy, getCurrentTile());
            }
        }
    }

    StringProperty cursorInfo = new SimpleStringProperty();

    public StringProperty cursorInfoProperty() {
        return cursorInfo;
    }

    public static Rectangle selectRect = null;
    public double selectScreenStartX = 0;
    public double selectScreenStartY = 0;

    private void startSelection(double x, double y) {
        selectRect = new Rectangle(1, 1, Color.NAVY);
        selectRect.setTranslateX(x);
        selectRect.setTranslateY(y);
        selectRect.setOpacity(0.5);
        selectScreenStartX = x;
        selectScreenStartY = y;
        anchorPane.getChildren().add(selectRect);
    }

    private void updateSelection(double x, double y) {
        if (selectRect == null || selectScreenStartX < 0 || selectScreenStartY < 0) {
            clearSelection();
            startSelection(x, y);
        }

        double minX = Math.min(selectScreenStartX, x);
        double minY = Math.min(selectScreenStartY, y);
        double maxX = Math.max(selectScreenStartX, x);
        double maxY = Math.max(selectScreenStartY, y);
        selectRect.setTranslateX(minX);
        selectRect.setTranslateY(minY);
        selectRect.setWidth(maxX - minX);
        selectRect.setHeight(maxY - minY);
        setSelectionArea(
            (int) (minX / tileWidth + posX),
            (int) (minY / tileHeight + posY),
            (int) (maxX / tileWidth + posX),
            (int) (maxY / tileHeight + posY)
        );
    }

    private void fillSelection() {
        for (int yy = startY; yy <= endY; yy++) {
            for (int xx = startX; xx <= endX; xx++) {
                plot(xx, yy, getCurrentTile());
            }
        }
        clearSelection();
    }

    private void clearSelection() {
        anchorPane.getChildren().remove(selectRect);
        selectRect = null;
        setSelectionArea(-1, -1, -1, -1);
    }

    public static int lastX = -1;
    public static int lastY = -1;
    DrawMode lastDrawMode = null;
    Tile lastTile = null;

    @Override
    protected void trackState() {
        currentMap.updateBackingMap();
        super.trackState();
    }

    @Override
    public void handle(MouseEvent t) {
        int x = (int) (t.getX() / tileWidth) + posX;
        int y = (int) (t.getY() / tileHeight) + posY;
        updateCursorAssistant(t);
        cursorInfo.set("X=" + x + " Y=" + y);
        if (!t.isPrimaryButtonDown() && t.getEventType() != MouseEvent.MOUSE_RELEASED && drawMode != DrawMode.FilledRect) {
            return;
        }
        if (getCurrentTile() == null && drawMode.requiresCurrentTile()) {
            return;
        }
        t.consume();
        boolean canSkip = false;
        if (getCurrentTile() == lastTile && x == lastX && y == lastY && drawMode == lastDrawMode) {
            canSkip = true;
        }
        lastX = x;
        lastY = y;
        lastDrawMode = drawMode;
        lastTile = getCurrentTile();
        switch (drawMode) {
            case TileEraser: {
                if (canSkip) {
                    return;
                }
                trackState();
                plot(x, y, null);
                redraw();
                break;
            }
            case Pencil1px:
                if (canSkip) {
                    return;
                }
                trackState();
                plot(x, y, getCurrentTile());
                break;
            case Pencil3px:
                if (canSkip) {
                    return;
                }
                trackState();
                drawBrush(x, y, 2, getCurrentTile());
                break;
            case Pencil5px:
                if (canSkip) {
                    return;
                }
                trackState();
                drawBrush(x, y, 5, getCurrentTile());
                break;
            case FilledRect:
                updateSelection(t.getX(), t.getY());
                if (t.getEventType().equals(MouseEvent.MOUSE_RELEASED)) {
                    trackState();
                    fillSelection();
                }
            case ScriptPencil:
                if (canSkip) {
                    return;
                }
                trackState();
                drawScript(x, y, getSelectedScript());
                break;
            case ScriptEraser:
                if (canSkip || !hasScriptsAt(x, y)) {
                    return;
                }
                trackState();
                drawScript(x, y, null);
                break;
            case Select:
                updateSelection(t.getX(), t.getY());
                if (t.getEventType().equals(MouseEvent.MOUSE_RELEASED)) {
                    selectScreenStartX = selectScreenStartY = -1;
                }

        }
    }

    @Override
    public void copyEntityFrom(Map src) {
        Map dest = getEntity();
        dest.setCategory(src.getCategory());
        dest.setDisplay3D(src.isDisplay3D());
        dest.setHeight(src.getHeight());
        dest.setName(src.getName());
        dest.setOrder(src.getOrder());
        dest.setScripts(src.getScripts());
        dest.setStartX(src.getStartX());
        dest.setStartY(src.getStartY());
        dest.setVariables(src.getVariables());
        dest.setWidth(src.getWidth());
        dest.getChunk().clear();
        dest.getChunk().addAll(src.getChunk());
    }
}
