import java.net.URL;

import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.SnapshotParameters;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.ClipboardContent;
import javafx.scene.input.Dragboard;
import javafx.scene.input.KeyCode;
import javafx.scene.input.TransferMode;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.StrokeType;

/**
 * The Klondike table as a view class. It listens to the game and redraws itself,
 * and passes what the user wants to its controller. It never changes the game itself.
 */
public class GameView extends StackPane implements KlondikeView, GameListener {
    private static final double GAP = 10;
    private static final double TABLE_WIDTH = 600;
    private static final double TABLE_HEIGHT = 380;
    private static final double TABLEAU_WIDTH =
        KlondikeGame.COLUMNS * CardView.WIDTH + (KlondikeGame.COLUMNS - 1) * GAP;

    private KlondikeController controller_;  // set by start(); receives the user's intentions

    private CardView stock_;
    private CardView waste_;
    private StackPane[] columns_ = new StackPane[KlondikeGame.COLUMNS];
    private Rectangle[] outlines_ = new Rectangle[KlondikeGame.COLUMNS];
    private Rectangle[] rings_ = new Rectangle[KlondikeGame.COLUMNS];
    private StackPane tableauLayer_;
    private Rectangle hint_;
    private Label message_;
    private int cursor_ = 0;                 // keyboard cursor: view state, not game state
    private KlondikeGame shown_;             // the game last drawn, read for accessible text

    public GameView() {
        stock_ = new CardView("", false);
        stock_.setFaceUp(false);
        stock_.setCursor(Cursor.HAND);
        stock_.setAccessibleText("Stock: click to draw");
        stock_.setOnMouseClicked(e -> controller_.drawFromStock());

        waste_ = new CardView("", false);
        setUpDragSource();
        HBox topRow = new HBox(GAP, stock_, waste_);

        HBox tableau = new HBox(GAP);
        for (int i = 0; i < KlondikeGame.COLUMNS; i++) {
            columns_[i] = makeColumn(i);
            tableau.getChildren().add(columns_[i]);
        }
        setUpDropTargets();

        hint_ = new Rectangle(TABLEAU_WIDTH, CardView.HEIGHT, Color.rgb(255, 255, 255, 0.25));
        hint_.setMouseTransparent(true);
        tableauLayer_ = new StackPane(tableau, hint_);
        tableauLayer_.setAlignment(Pos.TOP_LEFT);
        setUpKeyboard();

        message_ = new Label();
        message_.setTextFill(Color.WHITE);
        Label keys = new Label("Keyboard: Tab to the table (or click it), then ← / → choose a column, "
                + "Enter plays the waste card, D draws");
        keys.setTextFill(Color.WHITE);

        CheckBox showHint = new CheckBox("Show hint band");
        showHint.setTextFill(Color.WHITE);
        hint_.visibleProperty().bind(showHint.selectedProperty());   // a binding instead of a handler

        VBox piles = new VBox(2 * GAP, topRow, tableauLayer_, message_, keys, showHint);
        piles.setPadding(new Insets(20));

        getChildren().addAll(makeBackground(), piles);
        setAlignment(Pos.TOP_LEFT);
        setPrefSize(TABLE_WIDTH, TABLE_HEIGHT);
    }

    // ---- the controller this view passes intentions to ----

    public void setController(KlondikeController controller) {
        controller_ = controller;
    }

    // ---- GameListener: the game changed, so redraw ----

    @Override
    public void gameChanged(KlondikeGame game) {
        refresh(game);
    }

    /** Redraws every pile from the game's state. */
    public void refresh(KlondikeGame game) {
        shown_ = game;
        if (game.getStockSize() > 0) {
            stock_.setOpacity(1.0);
        } else {
            stock_.setOpacity(0.3);            // still clickable: recycles the waste
        }

        Card top = game.getWasteTop();
        if (top == null) {
            waste_.setVisible(false);
        } else {
            waste_.setCard(top.toString(), top.isRed());
            waste_.setAccessibleText("Waste: " + top.getSpokenName());
            waste_.setVisible(true);
        }

        for (int i = 0; i < KlondikeGame.COLUMNS; i++) {
            columns_[i].getChildren().setAll(outlines_[i]);   // keep the column; rebuild its contents
            Card card = game.getColumnTop(i);
            if (card != null) {
                columns_[i].getChildren().add(new CardView(card.toString(), card.isRed()));
            }
            columns_[i].getChildren().add(rings_[i]);
        }
        showCursor();
    }

    // ---- KlondikeView: what the controller may ask for ----

    @Override
    public void showMessage(String text) {
        message_.setText(text);
    }

    // ---- building ----

    private Node makeBackground() {
        URL url = getClass().getResource("/images/felt.png");
        if (url == null) {
            return new Rectangle(TABLE_WIDTH, TABLE_HEIGHT, Color.DARKGREEN);
        }
        ImageView felt = new ImageView(new Image(url.toExternalForm()));
        felt.setFitWidth(TABLE_WIDTH);
        felt.setFitHeight(TABLE_HEIGHT);
        return felt;
    }

    private StackPane makeColumn(int i) {
        outlines_[i] = new Rectangle(CardView.WIDTH, CardView.HEIGHT, Color.rgb(0, 0, 0, 0.15));
        outlines_[i].setArcWidth(10);
        outlines_[i].setArcHeight(10);
        outlines_[i].setStrokeType(StrokeType.INSIDE);
        outlines_[i].setStroke(Color.rgb(255, 255, 255, 0.5));

        rings_[i] = new Rectangle(CardView.WIDTH, CardView.HEIGHT);
        rings_[i].setFill(null);
        rings_[i].setArcWidth(10);
        rings_[i].setArcHeight(10);
        rings_[i].setStrokeType(StrokeType.INSIDE);
        rings_[i].setStroke(Color.DODGERBLUE);
        rings_[i].setStrokeWidth(4);
        rings_[i].setVisible(false);
        rings_[i].setMouseTransparent(true);

        StackPane column = new StackPane();
        column.setMinSize(CardView.WIDTH, CardView.HEIGHT);
        column.setPrefSize(CardView.WIDTH, CardView.HEIGHT);
        column.setMaxSize(CardView.WIDTH, CardView.HEIGHT);
        return column;
    }

    // ---- input: turn gestures into intentions ----

    private void setUpDragSource() {
        waste_.setOnDragDetected(e -> {
            Dragboard db = waste_.startDragAndDrop(TransferMode.MOVE);
            ClipboardContent content = new ClipboardContent();
            content.putString("waste");
            db.setContent(content);

            SnapshotParameters params = new SnapshotParameters();
            params.setFill(Color.TRANSPARENT);
            db.setDragView(waste_.snapshot(params, null), CardView.WIDTH / 2, CardView.HEIGHT / 2);

            waste_.setOpacity(0.4);
            e.consume();
        });
        waste_.setOnDragDone(e -> {
            waste_.setOpacity(1.0);
            e.consume();
        });
    }

    private void setUpDropTargets() {
        for (int i = 0; i < KlondikeGame.COLUMNS; i++) {
            final int col = i;
            StackPane target = columns_[i];

            target.setOnDragOver(e -> {
                if (e.getGestureSource() != target && e.getDragboard().hasString()) {
                    e.acceptTransferModes(TransferMode.MOVE);
                }
                e.consume();
            });
            target.setOnDragEntered(e -> {
                setColumnHighlight(col, true);
                e.consume();
            });
            target.setOnDragExited(e -> {
                setColumnHighlight(col, false);
                e.consume();
            });
            target.setOnDragDropped(e -> {
                controller_.playWasteTo(col);   // the intention; the controller decides
                e.setDropCompleted(true);      // the gesture finished; legality is not the view's business
                e.consume();
            });
        }
    }

    private void setUpKeyboard() {
        tableauLayer_.setFocusTraversable(true);
        tableauLayer_.focusedProperty().addListener((obs, wasFocused, isFocused) -> showCursor());
        tableauLayer_.setOnMouseClicked(e -> tableauLayer_.requestFocus());
        tableauLayer_.setOnKeyPressed(e -> {
            if (e.getCode() == KeyCode.LEFT) {
                if (cursor_ > 0) {
                    cursor_--;
                }
            } else if (e.getCode() == KeyCode.RIGHT) {
                if (cursor_ < KlondikeGame.COLUMNS - 1) {
                    cursor_++;
                }
            } else if (e.getCode() == KeyCode.ENTER) {
                controller_.playWasteTo(cursor_);  // same intention as a drop
            } else if (e.getCode() == KeyCode.D) {
                controller_.drawFromStock();
            } else {
                return;                           // leave other keys alone, so Tab still works
            }
            showCursor();
            e.consume();
        });
    }

    // ---- view state ----

    private void setColumnHighlight(int col, boolean on) {
        rings_[col].setVisible(on || (col == cursor_ && tableauLayer_.isFocused()));
    }

    private void showCursor() {
        boolean focused = tableauLayer_.isFocused();
        for (int i = 0; i < KlondikeGame.COLUMNS; i++) {
            rings_[i].setVisible(focused && i == cursor_);
        }
        if (shown_ != null) {
            tableauLayer_.setAccessibleText("Column " + (cursor_ + 1) + " of " + KlondikeGame.COLUMNS
                    + ": " + describe(shown_.getColumnTop(cursor_))
                    + ". Waste: " + describe(shown_.getWasteTop()) + ".");
        }
    }

    private String describe(Card card) {
        if (card == null) {
            return "empty";
        }
        return card.getSpokenName();
    }
}
