package hirondelle.starfield.physics;

import hirondelle.starfield.projection.Coords;
import hirondelle.starfield.projection.Projection;
import hirondelle.starfield.projection.Projector;

import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Shape;
import java.awt.geom.Ellipse2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;

import javax.imageio.ImageIO;

/** 
  Dynamically generate an image of a starfield to an output image file, one star at a time. 
  <P>The following data is rendered for each visible star: its magnitude (brightness), color (surface temperature), and position.
*/
final class StarfieldImage {

   /**
    Constructor.
    
    @param aProjector how to position each star on the image.
    @param aOutputFile where the file will be saved on the local file system. The extension of the file name determines the
    type of file generated - jpg, png, etc. The file format .png seems to give the best results.
    @param aImageSize the width (and height) of the image in pixels; the generated image is square. If this is too large, 
    then your host may run out of memory.
    @param aMagnification applies only to the half-sky projections. Magnifying to ~6x gives a nice result for high beta.
  */
  StarfieldImage(Projector aProjector, File aOutputFile, int aImageSize, int aMagnification){
    fImageSize = aImageSize;
    fMagnification = aMagnification;
    fCenter = new Coords(aImageSize/2, aImageSize/2);
    if (aProjector.isHalfSky()){
      fScale = mapScaleHalfSky(aImageSize, fMagnification);
    }
    else {
      fScale = mapScaleFullSky(aImageSize);
    }
    fProjection = aProjector.getProjection();
    fOutputFile = aOutputFile;
    initializeImage(aProjector);
  }

  /** 
   Add a boosted star to the generated image.
  */
  void add(BoostedStar aBoostedStar, double aPhi /*right ascension*/){
    if (fCtx == null){
      throw new IllegalStateException("Image file has already been generated. Cannot add another star.");
    }
    Coords coords = fProjection.project(aBoostedStar.ThetaPrime, aPhi, fScale, fCenter);
    //Util.log("Image coords: " + coords);
    fCtx.setColor(Star.temperatureToColor(aBoostedStar.Temperature));
    if (aBoostedStar.Magnitude > 4.5){
      point(aBoostedStar, coords);
    }
    else {
      square(aBoostedStar, coords);
    }
    //the spots are jagged; not used here
    fCtx.setColor(FOREGROUND_COLOR);
  }
  
  /** 
   Notify this class that no more stars are to be added, and to generate 
   the final file as output. This method calls {@link #dispose()} at the end. 
  */
  void generateFile() {
    try {
      ImageIO.write(fBufferedImage, fileExtension(), fOutputFile);
    }
    catch (IOException ex) {
      ex.printStackTrace();
    }
    dispose();
  }
  
  /** Clean up resources when finished with this object. */
  void dispose(){
    if (fCtx == null){
      fCtx.dispose();
      fCtx = null; //acts as a flag
    }
  }

  // PRIVATE

  private int fImageSize;
  private int fMagnification = 1; //6x is nice for the half-sky projections
  private final Coords fCenter;
  private Projection fProjection;
  private File fOutputFile;
  private double fScale = 0.0;
  private BufferedImage fBufferedImage;
  private Graphics2D fCtx;

  private static double mapScaleFullSky(int aImageSize){
    return 0.125D * aImageSize;
  }
  
  private static double mapScaleHalfSky(int aImageSize, int aMagnification){
    return 0.5D * aImageSize * aMagnification;
  }
  
  private static final Color BACKGROUND_COLOR = new Color(50,50,50);
  private static final Color FOREGROUND_COLOR = Color.yellow;
  
  /** Initial size, background color, author-text. */
  private void initializeImage(Projector aProjector){
    fBufferedImage = new BufferedImage(fImageSize, fImageSize, BufferedImage.TYPE_INT_RGB);
    fCtx = fBufferedImage.createGraphics();

    //text in the upper left
    //fCtx.setColor(FOREGROUND_COLOR);
    //fCtx.drawString("John O'Hanley, specialrelativity.net", 10, 20);

    //circle and clipping area for the half-sky projections
    if (aProjector.isHalfSky()){
      fCtx.setColor(BACKGROUND_COLOR);
      fCtx.fillRect(0, 0, fImageSize, fImageSize);
      
      //assumes the width is greater than or equal to the height
      fCtx.setColor(Color.black);
      fCtx.fillOval(fImageSize/2-fImageSize/2, 0, fImageSize, fImageSize);
      //set a clipping area, to limit the area to which items are rendered; this will ignore items with coords outside the circle
      Shape circle = new Ellipse2D.Double(fImageSize/2-fImageSize/2, 0,fImageSize, fImageSize);
      fCtx.setClip(circle);
      fCtx.setColor(FOREGROUND_COLOR);
    }
  }
  
  private String fileExtension(){
    int lastDot = fOutputFile.getName().lastIndexOf(".");
    return fOutputFile.getName().substring(lastDot+1);
  }

  /** Single pixel. */
  private void point(BoostedStar aBoostedStar, Coords coords){
    //single pixel - can't see any difference in these various techniques
    fCtx.drawLine((int)coords.X, (int)coords.Y, (int)coords.X, (int)coords.Y);
    
    //the ctx uses the top left as its origin, not the center of the shape
    //fCtx.fillRect((int)coords.X-1, (int)coords.Y-1, 1, 1);
    
    //fBufferedImage.setRGB((int)coords.X, (int)coords.Y, FOREGROUND_COLOR.getRGB() );
    //Later, in generate: fCtx.drawImage(fBufferedImage, 0, 0, null);
  }

  private void square(BoostedStar aBoostedStar, Coords coords){
    double size = magToSquareSize(aBoostedStar.Magnitude);
    //int size = 1; //fixed size!!
    int x = (int)(coords.X - size); //top left
    int y = (int)(coords.Y - size);
    fCtx.fillRect(x, y, (int)size, (int)size);
  }

  //spots are rendered very poorly; chunky integers? 
  private void spot(BoostedStar aBoostedStar, Coords coords){
    int x = (int)(coords.X);
    int y = (int)(coords.Y);
    fCtx.fillOval(x, y, 20,20);
  }
  
  private void spot1(BoostedStar aBoostedStar, Coords coords){
    double size = magToSpotSize(aBoostedStar.Magnitude);
    int x = (int)(coords.X - size);
    int y = (int)(coords.Y - size);
    fCtx.fillOval(x, y, (int)size*2, (int) size*2);
  }
  
  /** Return width/height of the square. */
  private double magToSquareSize(double mag){
    double result=0;
    double MAG_0_SIZE=3;
    double MAG_5_SIZE=1;
    double BASE_RANGE=MAG_0_SIZE-MAG_5_SIZE;
    result=MAG_0_SIZE - mag * (BASE_RANGE/5.0D);
    return result;
  }
  
  /** Return radius of the circle. */
  private double magToSpotSize(double aMagnitude){
    double result=0;
    double MAG_0_SIZE = 0.75;
    double MAG_5_SIZE = 0.1;
    double BASE_RANGE = MAG_0_SIZE - MAG_5_SIZE;
    result = MAG_0_SIZE - aMagnitude * (BASE_RANGE/5.0D);
    return result;
  }
}