import java.net.URL;

import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.Scene;
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;
import javafx.stage.Stage;

public class KlondikeTable extends Application {
	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 =
	    MiniGame.COLUMNS * CardView.WIDTH + (MiniGame.COLUMNS - 1) * GAP;

	private MiniGame game_ = new MiniGame(42);
	private CardView stock_;
	private CardView waste_;
	private StackPane[] columns_ = new StackPane[MiniGame.COLUMNS];
	private Rectangle[] outlines_ = new Rectangle[MiniGame.COLUMNS];
	private Rectangle[] rings_ = new Rectangle[MiniGame.COLUMNS];
	private StackPane tableauLayer_;
	private Rectangle hint_;
	private Label message_;
	private int cursor_ = 0; // keyboard cursor: which column

	@Override
	public void start ( Stage stage ) {
		// stock: a face-down card you click
		stock_ = new CardView("",false);
		stock_.setFaceUp(false);
		stock_.setCursor(Cursor.HAND);
		stock_.setOnMouseClicked(e -> drawFromStock());

		// waste: the drag source
		waste_ = new CardView("",false);
		setUpDragSource();
		HBox topRow = new HBox(GAP,stock_,waste_);

		// tableau: the drop targets
		HBox tableau = new HBox(GAP);
		for ( int i = 0 ; i < MiniGame.COLUMNS ; i++ ) {
			columns_[i] = makeColumn(i);
			tableau.getChildren().add(columns_[i]);
		}
		setUpDropTargets();

		// hint band over the tableau row
		hint_ = new Rectangle(TABLEAU_WIDTH,CardView.HEIGHT,
		                      Color.rgb(255,255,255,0.25));
		hint_.setVisible(false);
		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);
		showHint.setOnAction(e -> hint_.setVisible(showHint.isSelected()));

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

		// layers, bottom to top
		StackPane table = new StackPane(makeBackground(),piles);
		table.setAlignment(Pos.TOP_LEFT);
		table.setPrefSize(TABLE_WIDTH,TABLE_HEIGHT);

		refreshTable();
		stage.setScene(new Scene(table));
		stage.setTitle("Klondike table");
		stage.setResizable(false);
		stage.show();
	}

	/**
	 * The felt image from resources/images/felt.png if present; otherwise plain
	 * green.
	 */
	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;
	}

	/**
	 * An empty slot outline plus a highlight ring; the card itself is added by
	 * refreshTable().
	 */
	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); // decoration

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

	private void setUpDragSource () {
		waste_.setOnDragDetected(e -> {
			if ( game_.getWasteTop() == null ) {
				return;
			}
			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 < MiniGame.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 -> {
				boolean moved = playWasteTo(col); // same action as the Enter key
				e.setDropCompleted(moved);
				e.consume();
			});
		}
	}

	/** Keyboard alternative to dragging (L21 slide 15). */
	private void setUpKeyboard () {
		tableauLayer_.setFocusTraversable(true);
		// show the cursor ring whenever the board gains or loses focus
		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_ < MiniGame.COLUMNS - 1 ) {
					cursor_++;
				}
			} else if ( e.getCode() == KeyCode.ENTER ) {
				playWasteTo(cursor_);
			} else if ( e.getCode() == KeyCode.D ) {
				drawFromStock();
			} else {
				return; // leave other keys alone, so Tab still works
			}
			showCursor();
			e.consume();
		});
	}

	/** One action, reached by dragging or by the keyboard. */
	private boolean playWasteTo ( int col ) {
		boolean moved = game_.moveWasteTo(col);
		if ( moved ) {
			message_.setText("");
			refreshTable();
		} else {
			message_.setText(game_.getLastError());
		}
		return moved;
	}

	/**
	 * Drag highlight; when a drag leaves the cursor column, the cursor ring
	 * stays.
	 */
	private void setColumnHighlight ( int col, boolean on ) {
		rings_[col].setVisible(on || (col == cursor_ && tableauLayer_.isFocused()));
	}

	/**
	 * Shows the ring on the cursor column while the board has focus, and tells
	 * screen readers where the cursor is.
	 */
	private void showCursor () {
		boolean focused = tableauLayer_.isFocused();
		for ( int i = 0 ; i < MiniGame.COLUMNS ; i++ ) {
			rings_[i].setVisible(focused && i == cursor_);
		}
		tableauLayer_.setAccessibleText("Column " + (cursor_ + 1) + " of "
		    + MiniGame.COLUMNS + ": " + describe(game_.getColumnTop(cursor_))
		    + ". Waste: " + describe(game_.getWasteTop()) + ".");
	}

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

	private void drawFromStock () {
		game_.drawFromStock();
		message_.setText("");
		refreshTable();
	}

	/** Redraws every pile from the game's state. */
	private void refreshTable () {
		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_.setVisible(true);
		}

		for ( int i = 0 ; i < MiniGame.COLUMNS ; i++ ) {
			columns_[i].getChildren().setAll(outlines_[i]);
			Card card = game_.getColumnTop(i);
			if ( card != null ) {
				columns_[i].getChildren()
				    .add(new CardView(card.toString(),card.isRed()));
			}
			columns_[i].getChildren().add(rings_[i]);
		}
	}

	public static void main ( String[] args ) {
		launch(args);
	}
}