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.Button;
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_; // receives the card moves
	private NavigationController navigation_; // receives "leave this screen"

	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

		Button newDeal = new Button("_New deal");
		newDeal.setOnAction(e -> navigation_.newDeal());
		HBox controls = new HBox(2 * GAP,showHint,newDeal);
		controls.setAlignment(Pos.CENTER_LEFT);

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

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

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

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

	public void setNavigation ( NavigationController navigation ) {
		navigation_ = navigation;
	}

	// ---- 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();
	}
}
