// ============================================================================
// File:               $File$
//
// Project:            
//
// Purpose:            
//
// Author:             Rammi
//
// Copyright Notice:   (c) 2008  Rammi (rammi@caff.de)
//                     This code is in the public domain.
//                     Use at own risk.
//                     No guarantees given.
//
// Latest change:      $Date$
//
// History:	       $Log$
//=============================================================================
package de.caff.asteroid;

import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

/**
 *  A text (or number) displayed on screen.
 *
 *  This class is part of a solution for a
 *  <a href="http://www.heise.de/ct/creativ/08/02/details/">competition by the German computer magazine c't</a>
 */
public class Text
        extends GameObject
        implements Drawable
{
  /** The stadard text color. */
  public static final Color TEXT_COLOR = Color.gray;
  /** Mapping of characters to glyphs. */
  private static final Map<Character, Glyph> GLYPH_MAP = new HashMap<Character, Glyph>();
  static {
    GLYPH_MAP.put('A',            // 0xA78
                 new Glyph(new byte[] {0, -8, 7, -4, -12, 7, -8, -8, 7, -8, 0, 7, 0, -4, 0, -8, -4, 7, -12, 0, 0}));
    GLYPH_MAP.put('B',            // 0xA80
                 new Glyph(new byte[] {0, -12, 7, -6, -12, 7, -8, -10, 7, -8, -8, 7, -6, -6, 7, 0, -6, 7, -6, -6, 0, -8, -4, 7, -8, -2, 7, -6, 0, 7, 0, 0, 7, -12, 0, 0}));
    GLYPH_MAP.put('C',            // 0xA8D
                 new Glyph(new byte[] {0, -12, 7, -8, -12, 7, 0, 0, 0, -8, 0, 7, -12, 0, 0}));
    GLYPH_MAP.put('D',            // 0xA93
                 new Glyph(new byte[] {0, -12, 7, -4, -12, 7, -8, -8, 7, -8, -4, 7, -4, 0, 7, 0, 0, 7, -12, 0, 0}));
    GLYPH_MAP.put('E',            // 0xA9B
                 new Glyph(new byte[] {0, -12, 7, -8, -12, 7, -6, -6, 0, 0, -6, 7, 0, 0, 0, -8, 0, 7, -12, 0, 0}));
    GLYPH_MAP.put('F',            // 0xAA3
                 new Glyph(new byte[] {0, -12, 7, -8, -12, 7, -6, -6, 0, 0, -6, 7, 0, 0, 0, -12, 0, 0}));
    GLYPH_MAP.put('G',            // 0xAAA
                 new Glyph(new byte[] {0, -12, 7, -8, -12, 7, -8, -8, 7, -4, -4, 0, -8, -4, 7, -8, 0, 7, 0, 0, 7, -12, 0, 0}));
    GLYPH_MAP.put('H',            // 0xAB3
                 new Glyph(new byte[] {0, -12, 7, 0, -6, 0, -8, -6, 7, -8, -12, 0, -8, 0, 7, -12, 0, 0}));
    GLYPH_MAP.put('I',            // 0xABA
                 new Glyph(new byte[] {-8, 0, 7, -4, 0, 0, -4, -12, 7, -8, -12, 0, 0, -12, 7, -12, 0, 0}));
    GLYPH_MAP.put('J',            // 0xAC1
                 new Glyph(new byte[] {0, -4, 0, -4, 0, 7, -8, 0, 7, -8, -12, 7, -12, 0, 0}));
    GLYPH_MAP.put('K',            // 0xAC7
                 new Glyph(new byte[] {0, -12, 7, -6, -12, 0, 0, -6, 7, -6, 0, 7, -12, 0, 0}));
    GLYPH_MAP.put('L',            // 0xACD
                 new Glyph(new byte[] {0, -12, 0, 0, 0, 7, -8, 0, 7, -12, 0, 0}));
    GLYPH_MAP.put('M',            // 0xAD2
                 new Glyph(new byte[] {0, -12, 7, -4, -8, 7, -8, -12, 7, -8, 0, 7, -12, 0, 0}));
    GLYPH_MAP.put('N',            // 0xAD8
                 new Glyph(new byte[] {0, -12, 7, -8, 0, 7, -8, -12, 7, -12, 0, 0}));
    GLYPH_MAP.put('0',            // 0xADD  (zero, not oh)
                 new Glyph(new byte[] {0, -12, 7, -8, -12, 7, -8, 0, 7, 0, 0, 7, -12, 0, 0}));
    GLYPH_MAP.put('P',            // 0xAE3
                 new Glyph(new byte[] {0, -12, 7, -8, -12, 7, -8, -6, 7, 0, -6, 7, -6, 0, 0, -12, 0, 0}));
    GLYPH_MAP.put('Q',            // 0xAEA
                 new Glyph(new byte[] {0, -12, 7, -8, -12, 7, -8, -4, 7, -4, 0, 7, 0, 0, 7, -4, -4, 0, -8, 0, 7, -12, 0, 0}));
    GLYPH_MAP.put('R',            // 0xAF3
                 new Glyph(new byte[] {0, -12, 7, -8, -12, 7, -8, -6, 7, 0, -6, 7, -2, -6, 0, -8, 0, 7, -12, 0, 0}));
    GLYPH_MAP.put('S',            // 0xAFB
                 new Glyph(new byte[] {-8, 0, 7, -8, -6, 7, 0, -6, 7, 0, -12, 7, -8, -12, 7, -12, 0, 0}));
    GLYPH_MAP.put('T',            // 0xB02
                 new Glyph(new byte[] {-4, 0, 0, -4, -12, 7, 0, -12, 0, -8, -12, 7, -12, 0, 0}));
    GLYPH_MAP.put('U',            // 0xB08
                 new Glyph(new byte[] {0, -12, 0, 0, 0, 7, -8, 0, 7, -8, -12, 7, -12, 0, 0}));
    GLYPH_MAP.put('V',            // 0xB0E
                 new Glyph(new byte[] {0, -12, 0, -4, 0, 7, -8, -12, 7, -12, 0, 0}));
    GLYPH_MAP.put('W',            // 0xB13
                 new Glyph(new byte[] {0, -12, 0, 0, 0, 7, -4, -4, 7, -8, 0, 7, -8, -12, 7, -12, 0, 0}));
    GLYPH_MAP.put('X',            // 0xB1A
                 new Glyph(new byte[] {-8, -12, 7, 0, -12, 0, -8, 0, 7, -12, 0, 0}));
    GLYPH_MAP.put('Y',            // 0xB1F
                 new Glyph(new byte[] {-4, 0, 0, -4, -8, 7, 0, -12, 7, -8, -12, 0, -4, -8, 7, -12, 0, 0}));
    GLYPH_MAP.put('Z',            // 0xB26
                 new Glyph(new byte[] {0, -12, 0, -8, -12, 7, 0, 0, 7, -8, 0, 7, -12, 0, 0}));
    GLYPH_MAP.put(' ',            // 0xB2C
                 new Glyph(new byte[] {-12, 0, 0}, new Rectangle(0, 0, 8, 12)));
    GLYPH_MAP.put('1',            // 0xB2E
                 new Glyph(new byte[] {-4, 0, 0, -4, -12, 7, -12, 0, 0}));
    GLYPH_MAP.put('2',            // 0xB32
                 new Glyph(new byte[] {0, -12, 0, -8, -12, 7, -8, -6, 7, 0, -6, 7, 0, 0, 7, -8, 0, 7, -12, 0, 0}));
    GLYPH_MAP.put('3',            // 0xB3A
                 new Glyph(new byte[] {-8, 0, 7, -8, -12, 7, 0, -12, 7, 0, -6, 0, -8, -6, 7, -12, 0, 0}));
    GLYPH_MAP.put('4',            // 0xB41
                 new Glyph(new byte[] {0, -12, 0, 0, -6, 7, -8, -6, 7, -8, -12, 0, -8, 0, 7, -12, 0, 0}));
    GLYPH_MAP.put('5',            // 0xB48
                 new Glyph(new byte[] {-8, 0, 7, -8, -6, 7, 0, -6, 7, 0, -12, 7, -8, -12, 7, -12, 0, 0}));
    GLYPH_MAP.put('6',            // 0xB4F
                 new Glyph(new byte[] {0, -6, 0, -8, -6, 7, -8, 0, 7, 0, 0, 7, 0, -12, 7, -12, 0, 0}));
    GLYPH_MAP.put('7',            // 0xB56
                 new Glyph(new byte[] {0, -12, 0, -8, -12, 7, -8, 0, 7, -12, 0, 0}));
    GLYPH_MAP.put('8',            // 0xB5B
                 new Glyph(new byte[] {-8, 0, 7, -8, -12, 7, 0, -12, 7, 0, 0, 7, 0, -6, 0, -8, -6, 7, -12, 0, 0}));
    GLYPH_MAP.put('9',            // 0xB63
                 new Glyph(new byte[] {-8, 0, 0, -8, -12, 7, 0, -12, 7, 0, -6, 7, -8, -6, 7, -12, 0, 0}));

    GLYPH_MAP.put('\'',            // additional glyph used for c't
                 new Glyph(new byte[] {-4, -8, 0, -4, -12, 7, -12, 0, 0}));
  }

  /** The space glyph. */
  public static final Glyph SPACE_GLYPH = GLYPH_MAP.get(' ');
  /** Font to use for scaling 1. */
  public static final Font FONT = new Font("Monospaced", Font.PLAIN, 14);
  /** The text. */
  private final String text;
  /** The scale. */
  private final int scale;

  /**
   *  Constructor.
   *  @param text text
   *  @param x     positon x
   *  @param y     postion y
   *  @param scale text scaling
   */
  public Text(String text, int x, int y, int scale)
  {
    super(x, y);
    this.text = text;
    this.scale = (int)Math.pow(2.0, scale);
  }

  /**
   *  Get the displayed text.
   *  Take care that all text are only uppercase and use 0 (zero) instead of O (oh).
   *  @return text
   */
  public String getText()
  {
    return text;
  }

  /**
   * Returns a string representation of the object.
   *
   * @return a string representation of the object.
   */
  @Override
  public String toString()
  {
    return String.format("Text(%s)@(%d,%d)", text, getX(), getY());
  }

  /**
   *  Has this text a given location?
   *  @param px x coordinate
   *  @param py y coordinate
   *  @return <code>true</code> if this text is at the given location,
   *          <code>false</code> otherwise
   */
  public boolean hasLocation(int px, int py)
  {
    return px == x  &&  py == y;
  }

  /**
   *  Has this text a given location?
   *  @param p location
   *  @return <code>true</code> if this text is at the given location,
   *          <code>false</code> otherwise
   */
  public boolean hasLocation(Point p)
  {
    return hasLocation(p.x, p.y);
  }

  /**
   * Get the bounding box of this rectangle.
   *
   * @return the bounding box
   */
  public Rectangle getBounds()
  {
    Rectangle bounds = null;
    AffineTransform trafo = AffineTransform.getScaleInstance(scale, scale);
    Point offset = new Point(getX(), getY());
    for (char ch: text.toCharArray()) {
      Glyph glyph = GLYPH_MAP.get(ch);
      if (glyph == null) {
        glyph = SPACE_GLYPH;
      }
      Rectangle glyphBounds = glyph.getBounds(trafo);
      glyphBounds.translate(offset.x, offset.y);
      if (bounds == null) {
        bounds = glyphBounds;
      }
      else {
        bounds = bounds.union(glyphBounds);
      }
      Point glyphOffset = glyph.getOffset();
      offset.x += scale*glyphOffset.x;
      offset.y += scale*glyphOffset.y;
    }
    if (bounds == null) {
      // empty text
      bounds = new Rectangle(getX(), getY(), 1, 1);
    }
    return bounds;
  }

  /**
   *  Get the scale.
   *  @return scaling
   */
  public int getScale()
  {
    return scale;
  }

  /**
   *  Get a font which can be used to display the text.
   *  @return font
   */
  public Font getFont()
  {
    return FONT.deriveFont((float)(FONT.getSize() * scale));
  }

  /**
   * Get the properties of this object.
   *
   * @return collection of properties
   */
  @Override
  public Collection<Property> getProperties()
  {
    Collection<Property> props = super.getProperties();
    props.add(new Property<String>("Text", getText()));
    props.add(new Property<Integer>("Scale", getScale()));
    return props;
  }

  /**
   * Get the type of game object.
   *
   * @return game object type
   */
  public String getObjectType()
  {
    return "Text";
  }

  /**
   * Draw the object.
   *
   * @param g graphics context
   */
  public void draw(Graphics2D g)
  {
    Graphics2D g2 = (Graphics2D)g.create();
    g2.translate(x, y);
    g2.setColor(TEXT_COLOR);
    drawText(g2, text, scale);
  }

  /**
   *  Draw a text to a graphics context.
   *  @param g     graphics context
   *  @param text  text
   *  @param scale scaling
   */
  public static void drawText(Graphics2D g, String text, double scale)
  {
    for (char ch: text.toCharArray()) {
      Glyph glyph = GLYPH_MAP.get(ch);
      if (glyph == null) {
        glyph = SPACE_GLYPH;
      }
      glyph.drawAndTranslate(g, scale);
    }
  }

  /**
   *  Get a glyph for a given character.
   *  @param ch character
   *  @return glyph or <code>null</code> if there's no glyph for the given character
   */
  public static Glyph getGlyph(char ch)
  {
    return GLYPH_MAP.get(ch);
  }

  /**
   *  Get a glyph for a given character.
   *  @param ch character
   *  @param fallback default glyph
   *  @return glyph or <code>fallback</code> if there's no glyph for the given character
   */
  public static Glyph getGlyph(char ch, Glyph fallback)
  {
    Glyph glyph = getGlyph(ch);
    return glyph == null ? fallback : glyph;
  }

  /**
   *  Get the bounds for a given text in a given scaling.
   *  @param text   text to render
   *  @param scale  scaling
   *  @return bounds
   */
  public static Rectangle getTextBounds(String text, double scale)
  {
    Rectangle bounds = null;
    AffineTransform trafo = AffineTransform.getScaleInstance(scale, scale);
    Point offset = new Point();
    for (char ch: text.toCharArray()) {
      Glyph glyph = GLYPH_MAP.get(ch);
      if (glyph == null) {
        glyph = SPACE_GLYPH;
      }
      Rectangle glyphBounds = glyph.getBounds(trafo);
      glyphBounds.translate(offset.x, offset.y);
      if (bounds == null) {
        bounds = glyphBounds;
      }
      else {
        bounds = bounds.union(glyphBounds);
      }
      Point glyphOffset = glyph.getOffset();
      offset.x += scale*glyphOffset.x;
      offset.y += scale*glyphOffset.y;
    }
    return bounds;
  }

  /**
   *  Create an image with a text using the glyphs of the asteroid game.
   *  @param args various
   *  @throws IOException on write problems
   */
  public static void main(String[] args) throws IOException
  {
    if (args.length == 0) {
      System.out.println("Render text in asteroids font.");
      System.out.println("  Understanding the following arguments:");
      System.out.println("  [-a] [-b <border>] [-s <scale>] [-r <radius>] [-l <width>] <filename> text ...");
      System.out.println("\t-a             use antialias [Default: off]");
      System.out.println("\t-b <border>    border around text [Default: 20]");
      System.out.println("\t-s <scale>     scaling for text [Default: 1 (12 pixel)]");
      System.out.println("\t-r <radius>    corner radius [Default: 0]");
      System.out.println("\t-l <width>     line width");
      System.out.println("\t<filename>     name of output file (in PNG format)");
      System.out.println("\ttext           text to render");
      System.exit(0);
    }
    boolean antialias = false;
    String filename = null;
    String text = null;
    double scale = 1;
    int border = 20;
    int roundness = 0;
    int lineWidth = 1;
    for (int a = 0;  a < args.length;  ++a) {
      if ("-a".equals(args[a])) {
        antialias = true;
      }
      else if ("-b".equals(args[a])) {
        border = Integer.parseInt(args[++a]);
      }
      else if ("-s".equals(args[a])) {
        scale = Double.parseDouble(args[++a]);
      }
      else if ("-r".equals(args[a])) {
        roundness = Integer.parseInt(args[++a]);
      }
      else if ("-l".equals(args[a])) {
        lineWidth = Integer.parseInt(args[++a]);
      }
      else {
        if (filename == null) {
          filename = args[a];
        }
        else if (text == null) {
          text = args[a];
        }
        else {
          text += " "+args[a];
        }
      }
    }
    text = FrameInfo.canonize(text);
    Rectangle bounds = getTextBounds(text, scale);
    BufferedImage image = new BufferedImage(2*border + bounds.width,
                                            2*border + bounds.height,
                                            BufferedImage.TYPE_INT_ARGB);
    Graphics2D g = (Graphics2D)image.getGraphics();
    g.setColor(Color.black);
    g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                       antialias ? RenderingHints.VALUE_ANTIALIAS_ON : RenderingHints.VALUE_ANTIALIAS_OFF);
    g.setRenderingHint(RenderingHints.KEY_RENDERING,
                       RenderingHints.VALUE_RENDER_QUALITY);

    System.out.println("bounds="+bounds);
    System.out.println("image: "+image.getWidth()+"x"+image.getHeight());
    if (roundness > 0) {
      g.fillRoundRect(0, 0, image.getWidth(), image.getHeight(), roundness, roundness);
    }
    else {
      g.fillRect(0, 0, image.getWidth(), image.getHeight());
    }
    Point offset = new Point(bounds.x + (image.getWidth() - bounds.width)/2,
                             bounds.y + (image.getHeight() - bounds.height)/2 + bounds.height);
    g.translate(offset.x, offset.y);
    g.scale(1, -1);
    g.setStroke(new BasicStroke(lineWidth, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
    g.setColor(Color.gray);
    drawText(g, text, scale);

    ImageIO.write(image, "PNG", new File(filename));
    System.out.println("Written '"+text+"' to "+filename);
  }

  /**
   * Access method for visitor pattern.
   *
   * @param visitor visitor
   */
  public void visitedBy(GameObjectVisitor visitor)
  {
    visitor.handle(this);
  }
}
