package org.mage.card.arcane;
import com.google.common.collect.MapMaker;
import mage.cards.action.ActionCallback;
import mage.constants.CardType;
import mage.constants.SuperType;
import mage.view.CardView;
import mage.view.CounterView;
import mage.view.PermanentView;
import mage.view.StackAbilityView;
import net.java.truevfs.access.TFile;
import org.apache.log4j.Logger;
import org.jdesktop.swingx.graphics.GraphicsUtilities;
import org.mage.plugins.card.dl.sources.DirectLinksForDownload;
import org.mage.plugins.card.images.ImageCache;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.util.Map;
import java.util.UUID;
import static org.mage.plugins.card.constants.Constants.THUMBNAIL_SIZE_FULL;
public class CardPanelRenderImpl extends CardPanel {
private static final Logger LOGGER = Logger.getLogger(CardPanelRenderImpl.class);
private static boolean cardViewEquals(CardView a, CardView b) {
if (a == b) {
return true;
}
if (a.getClass() != b.getClass()) {
return false;
}
if (!a.getName().equals(b.getName())) {
return false;
}
if (!a.getPower().equals(b.getPower())) {
return false;
}
if (!a.getToughness().equals(b.getToughness())) {
return false;
}
if (!a.getLoyalty().equals(b.getLoyalty())) {
return false;
}
if (0 != a.getColor().compareTo(b.getColor())) {
return false;
}
if (!a.getCardTypes().equals(b.getCardTypes())) {
return false;
}
if (!a.getSubTypes().equals(b.getSubTypes())) {
return false;
}
if (!a.getSuperTypes().equals(b.getSuperTypes())) {
return false;
}
if (!a.getManaCost().equals(b.getManaCost())) {
return false;
}
if (!a.getRules().equals(b.getRules())) {
return false;
}
if (a.getRarity() == null || b.getRarity() == null) {
return false;
}
if (a.getRarity() != b.getRarity()) {
return false;
}
if (a.getCardNumber() != null && !a.getCardNumber().equals(b.getCardNumber())) {
return false;
}
// Expansion set code, with null checking:
// TODO: The null checks should not be necessary, but thanks to Issue #2260
// some tokens / commandobjects will be missing expansion set codes.
String expA = a.getExpansionSetCode();
if (expA == null) {
expA = "";
}
String expB = b.getExpansionSetCode();
if (expB == null) {
expB = "";
}
if (!expA.equals(expB)) {
return false;
}
if (a.getFrameStyle() != b.getFrameStyle()) {
return false;
}
if (a.getCounters() == null) {
if (b.getCounters() != null) {
return false;
}
} else if (!a.getCounters().equals(b.getCounters())) {
return false;
}
if (a.isFaceDown() != b.isFaceDown()) {
return false;
}
if ((a instanceof PermanentView)) {
PermanentView aa = (PermanentView) a;
PermanentView bb = (PermanentView) b;
if (aa.hasSummoningSickness() != bb.hasSummoningSickness()) {
// Note: b must be a permanentview too as we aleady checked that classes
// are the same for a and b
return false;
}
if (aa.getDamage() != bb.getDamage()) {
return false;
}
}
return true;
}
static class ImageKey {
final BufferedImage artImage;
final int width;
final int height;
final boolean isChoosable;
final boolean isSelected;
final CardView view;
final int hashCode;
public ImageKey(CardView view, BufferedImage artImage, int width, int height, boolean isChoosable, boolean isSelected) {
this.view = view;
this.artImage = artImage;
this.width = width;
this.height = height;
this.isChoosable = isChoosable;
this.isSelected = isSelected;
this.hashCode = hashCodeImpl();
}
private int hashCodeImpl() {
StringBuilder sb = new StringBuilder();
sb.append((char) (artImage != null ? 1 : 0));
sb.append((char) width);
sb.append((char) height);
sb.append((char) (isSelected ? 1 : 0));
sb.append((char) (isChoosable ? 1 : 0));
sb.append((char) (this.view.isPlayable() ? 1 : 0));
sb.append((char) (this.view.isCanAttack() ? 1 : 0));
sb.append((char) (this.view.isFaceDown() ? 1 : 0));
sb.append((char) this.view.getFrameStyle().ordinal());
if (this.view instanceof PermanentView) {
sb.append((char) (((PermanentView) this.view).hasSummoningSickness() ? 1 : 0));
sb.append((char) (((PermanentView) this.view).getDamage()));
}
sb.append(this.view.getName());
sb.append(this.view.getPower());
sb.append(this.view.getToughness());
sb.append(this.view.getLoyalty());
sb.append(this.view.getColor().toString());
sb.append(this.view.getExpansionSetCode());
for (CardType type : this.view.getCardTypes()) {
sb.append((char) type.ordinal());
}
for (SuperType s : this.view.getSuperTypes()) {
sb.append(s);
}
for (String s : this.view.getSubTypes()) {
sb.append(s);
}
for (String s : this.view.getManaCost()) {
sb.append(s);
}
for (String s : this.view.getRules()) {
sb.append(s);
}
if (this.view.getCounters() != null) {
for (CounterView v : this.view.getCounters()) {
sb.append(v.getName()).append(v.getCount());
}
}
return sb.toString().hashCode();
}
@Override
public int hashCode() {
return hashCode;
}
@Override
public boolean equals(Object object) {
// Initial checks
if (this == object) {
return true;
}
if (object == null) {
return false;
}
if (!(object instanceof ImageKey)) {
return false;
}
final ImageKey other = (ImageKey) object;
// Compare
if ((artImage != null) != (other.artImage != null)) {
return false;
}
if (width != other.width) {
return false;
}
if (height != other.height) {
return false;
}
if (isChoosable != other.isChoosable) {
return false;
}
if (isSelected != other.isSelected) {
return false;
}
return cardViewEquals(view, other.view);
}
}
// Map of generated images
private final static Map<ImageKey, BufferedImage> IMAGE_CACHE = new MapMaker().softValues().makeMap();
// The art image for the card, loaded in from the disk
private BufferedImage artImage;
// Factory to generate card appropriate views
private CardRendererFactory cardRendererFactory = new CardRendererFactory();
// The rendered card image, with or without the art image loaded yet
// = null while invalid
private BufferedImage cardImage;
private CardRenderer cardRenderer;
public CardPanelRenderImpl(CardView newGameCard, UUID gameId, final boolean loadImage, ActionCallback callback, final boolean foil, Dimension dimension) {
// Call to super
super(newGameCard, gameId, loadImage, callback, foil, dimension);
// Renderer
cardRenderer = cardRendererFactory.create(gameCard, isTransformed());
// Draw the parts
initialDraw();
}
@Override
public void transferResources(CardPanel panel) {
if (panel instanceof CardPanelRenderImpl) {
CardPanelRenderImpl impl = (CardPanelRenderImpl) panel;
// Use the art image and current rendered image from the card
artImage = impl.artImage;
cardRenderer.setArtImage(artImage);
cardImage = impl.cardImage;
}
}
@Override
protected void paintCard(Graphics2D g) {
// Render the card if we don't have an image ready to use
if (cardImage == null) {
// Try to get card image from cache based on our card characteristics
ImageKey key
= new ImageKey(gameCard, artImage,
getCardWidth(), getCardHeight(),
isChoosable(), isSelected());
cardImage = IMAGE_CACHE.computeIfAbsent(key, k -> renderCard());
// No cached copy exists? Render one and cache it
}
// And draw the image we now have
g.drawImage(cardImage, getCardXOffset(), getCardYOffset(), null);
}
/**
* Create an appropriate card renderer for the
*/
/**
* Render the card to a new BufferedImage at it's current dimensions
*
* @return
*/
private BufferedImage renderCard() {
int cardWidth = getCardWidth();
int cardHeight = getCardHeight();
// Create image to render to
BufferedImage image
= GraphicsUtilities.createCompatibleTranslucentImage(cardWidth, cardHeight);
Graphics2D g2d = image.createGraphics();
// Render with Antialialsing
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
// Attributes
CardPanelAttributes attribs
= new CardPanelAttributes(cardWidth, cardHeight, isChoosable(), isSelected());
// Draw card itself
cardRenderer.draw(g2d, attribs);
// Done
g2d.dispose();
return image;
}
private int updateArtImageStamp;
@Override
public void updateArtImage() {
// Invalidate
artImage = null;
cardImage = null;
cardRenderer.setArtImage(null);
// Stop animation
tappedAngle = isTapped() ? CardPanel.TAPPED_ANGLE : 0;
flippedAngle = isFlipped() ? CardPanel.FLIPPED_ANGLE : 0;
// Schedule a repaint
repaint();
// See if the image is already loaded
//artImage = ImageCache.tryGetImage(gameCard, getCardWidth(), getCardHeight());
//this.cardRenderer.setArtImage(artImage);
// Submit a task to draw with the card art when it arrives
if (artImage == null) {
final int stamp = ++updateArtImageStamp;
Util.threadPool.submit(() -> {
try {
final BufferedImage srcImage;
if (gameCard.isFaceDown()) {
// Nothing to do
srcImage = null;
} else if (getCardWidth() > THUMBNAIL_SIZE_FULL.width) {
srcImage = ImageCache.getImage(gameCard, getCardWidth(), getCardHeight());
} else {
srcImage = ImageCache.getThumbnail(gameCard);
}
UI.invokeLater(() -> {
if (stamp == updateArtImageStamp) {
artImage = srcImage;
cardRenderer.setArtImage(srcImage);
if (srcImage != null) {
// Invalidate and repaint
cardImage = null;
repaint();
}
}
});
} catch (Exception e) {
e.printStackTrace();
} catch (Error err) {
err.printStackTrace();
}
});
}
}
@Override
public void update(CardView card) {
// Update super
super.update(card);
// Update renderer
cardImage = null;
cardRenderer = cardRendererFactory.create(gameCard, isTransformed());
cardRenderer.setArtImage(artImage);
// Repaint
repaint();
}
@Override
public void setCardBounds(int x, int y, int cardWidth, int cardHeight) {
int oldCardWidth = getCardWidth();
int oldCardHeight = getCardHeight();
super.setCardBounds(x, y, cardWidth, cardHeight);
// Rerender if card size changed
if (getCardWidth() != oldCardWidth || getCardHeight() != oldCardHeight) {
cardImage = null;
}
}
private BufferedImage getFaceDownImage() {
if (isPermanent()) {
if (((PermanentView) gameCard).isMorphed()) {
return ImageCache.getMorphImage();
} else {
return ImageCache.getManifestImage();
}
} else if (this.gameCard instanceof StackAbilityView) {
return ImageCache.getMorphImage();
} else {
return ImageCache.loadImage(new TFile(DirectLinksForDownload.outDir + File.separator + DirectLinksForDownload.cardbackFilename));
}
}
@Override
public void setSelected(boolean selected) {
if (selected != isSelected()) {
super.setSelected(selected);
// Invalidate our render and trigger a repaint
cardImage = null;
repaint();
}
}
@Override
public void setChoosable(boolean choosable) {
if (choosable != isChoosable()) {
super.setChoosable(choosable);
// Invalidate our render and trigger a repaint
cardImage = null;
repaint();
}
}
@Override
public Image getImage() {
if (artImage != null) {
if (gameCard.isFaceDown()) {
return getFaceDownImage();
} else {
return ImageCache.getImageOriginal(gameCard);
}
}
return null;
}
@Override
public void showCardTitle() {
// Nothing to do, rendered cards always have a title
}
}