// ============================================================================
// File:               $File$
//
// Project:            
//
// Purpose:            
//
// Author:             Rammi
//
// Copyright Notice:   (c) 2008  Rammi (rammi@caff.de)
//                     The usage of this source code in commercial products
//                     is not allowed without explicite permission.
//
// Latest change:      $Date$
//
// History:	       $Log$
//=============================================================================
package de.caff.asteroid.rammi;

import de.caff.asteroid.analysis.DumpFile;
import de.caff.asteroid.analysis.FrameKeyInfo;
import de.caff.asteroid.*;

import javax.swing.*;
import java.awt.geom.*;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.ActionEvent;
import java.util.*;
import java.util.List;
import java.io.IOException;

/**
 *  Display for hit analysis data.
 */
public class HitAnalysisDisplay
        extends JComponent
{
  private static final int HIT_RADIUS = 2;
  private static final Color HIT_COLOR  = Color.red;
  private static final Color MISS_COLOR = Color.green;
  private static final int MAX_SHOW_SIZE = 128;
  private static final Rectangle2D SHOW_BOX = new Rectangle2D.Double(-2*MAX_SHOW_SIZE, -2*MAX_SHOW_SIZE, 4* MAX_SHOW_SIZE, 4* MAX_SHOW_SIZE);
  private static class LineInfo
  {
    private final Line2D line;
    private final int frameIndex;
    private final Bullet bullet;
    private final MovingGameObject object;
    private final boolean hit;

    public LineInfo(Line2D line, int frameIndex, Bullet bullet, MovingGameObject object, boolean hit)
    {
      this.line = line;
      this.frameIndex = frameIndex;
      this.bullet = bullet;
      this.object = object;
      this.hit    = hit;
    }

    public Line2D getLine()
    {
      return line;
    }

    public int getFrameIndex()
    {
      return frameIndex;
    }

    public Bullet getBullet()
    {
      return bullet;
    }

    public MovingGameObject getObject()
    {
      return object;
    }

    public boolean isHit()
    {
      return hit;
    }

    @Override
    public String toString()
    {
      return String.format("LineInfo(Line(%s, %s, %s, %s),\n"+
                           "         %6d, %4d, %4d, %s)",
                           Double.toString(line.getX1()), Double.toString(line.getY1()),
                           Double.toString(line.getX2()), Double.toString(line.getY2()),
                           frameIndex, bullet.getIdentity(), object.getIdentity(), hit);
    }
  }
  private static class HitAndMiss
  {
    private Collection<LineInfo> hitLines = new LinkedList<LineInfo>();
    private Collection<LineInfo> missLines = new LinkedList<LineInfo>();

    public void addHitLine(HitAnalysis.HitInfo hitInfo)
    {
      hitLines.add(new LineInfo(new Line2D.Double(hitInfo.getPreDestructionDelta(),
                                                  hitInfo.getDestructionDelta()),
                                hitInfo.getFrameIndex(),
                                hitInfo.getBullet(),
                                hitInfo.getObject(),
                                true));
    }

    public void addMissLine(Line2D line, int frameIndex, Bullet bullet, MovingGameObject object)
    {
      missLines.add(new LineInfo(line, frameIndex, bullet, object, false));
    }

    public Collection<LineInfo> getHitLines()
    {
      return Collections.unmodifiableCollection(hitLines);
    }

    public Collection<LineInfo> getMissLines()
    {
      return Collections.unmodifiableCollection(missLines);
    }
  }
  private static interface DataExtractor
  {
    HitAndMiss getHitAndMiss(DumpFile dumpFile, HitAnalysis hitAnalysis);
    int getSize();
  }

  private abstract class ShapePainter
  {
    private final Shape shape;

    protected ShapePainter(Shape shape)
    {
      this.shape = shape;
    }

    public void drawShape(Graphics2D g)
    {
      if (shape != null) {
        Shape drawShape = AffineTransform.getScaleInstance(extractor.getSize()+deltaR, extractor.getSize()+deltaR).createTransformedShape(shape);
        if (deltaX != 0  ||  deltaY != 0) {
          drawShape = AffineTransform.getTranslateInstance(deltaX, deltaY).createTransformedShape(drawShape);
        }
        g.setColor(Color.yellow);
        g.draw(drawShape);
        g.setColor(new Color(0xFF, 0xFF, 0x00, 0x40));
        g.fill(drawShape);
        g.setColor(new Color(0xFF, 0xFF, 0xFF));
        g.drawLine(-4, 0, 4, 0);
        g.drawLine(0, -4, 0, 4);
      }
    }

  }

  private class NullShapePainter
          extends ShapePainter
  {
    NullShapePainter()
    {
      super(null);
    }
  }

  private class RectangularShapePainter
          extends ShapePainter
  {
    RectangularShapePainter()
    {
      super(new Rectangle2D.Double(-1, -1, 2, 2));
    }
  }

  private class CircularShapePainter
          extends ShapePainter
  {
    CircularShapePainter()
    {
      super(new Ellipse2D.Double(-1, -1, 2, 2));
    }
  }

  static Shape createOctagon()
  {
    int size = 1;
    GeneralPath path = new GeneralPath();
    final float factor = 2;
    path.moveTo( size,         size/factor);
    path.lineTo( size/factor,  size);
    path.lineTo(-size/factor,  size);
    path.lineTo(-size,         size/factor);
    path.lineTo(-size,        -size/factor);
    path.lineTo(-size/factor, -size);
    path.lineTo( size/factor, -size);
    path.lineTo( size,        -size/factor);
    path.closePath();
    return path;
  }

  private class OctagonalShapePainter
          extends ShapePainter
  {
    OctagonalShapePainter()
    {
      super(createOctagon());
    }
  }

  private abstract static class AbstractBasicDataExtractor
          implements DataExtractor
  {
    public HitAndMiss getHitAndMiss(DumpFile dumpFile, HitAnalysis hitAnalysis)
    {
      HitAndMiss result = new HitAndMiss();
      List<HitAnalysis.HitInfo> hitInfos = new ArrayList<HitAnalysis.HitInfo>(getHitInfos(hitAnalysis));
      Map<Integer, java.util.List<Integer>> excludedObjectsByFrame = new HashMap<Integer, java.util.List<Integer>>();
      Collections.sort(hitInfos, HitAnalysis.FRAME_COMPARATOR);
      for (HitAnalysis.HitInfo info: hitInfos) {
        result.addHitLine(info);
        java.util.List<Integer> frameObjects = excludedObjectsByFrame.get(info.getFrameIndex());
        if (frameObjects == null) {
          frameObjects = new LinkedList<Integer>();
          excludedObjectsByFrame.put(info.getFrameIndex(), frameObjects);
        }
        frameObjects.add(info.getObject().getIdentity());
      }
      boolean first = true;
      for (FrameKeyInfo frameKeyInfo: dumpFile.getInfos()) {
        if (first) {
          first = false;
        }
        else {
          FrameInfo info = frameKeyInfo.getFrameInfo();
          java.util.List<Integer> excludedObjects = excludedObjectsByFrame.get(info.getIndex());
          for (MovingGameObject obj: getInterestingObjects(info)) {
            if (excludedObjects != null  &&
                excludedObjects.contains(obj.getIdentity())) {
              continue;
            }
            Point2D prevObjPos = obj.getCorrectedPredictedLocation(-1);
            for (Bullet bullet: info.getBullets()) {
              if (bullet.getLifetime() >= 16) {
                Point2D prevBulletPos = bullet.getCorrectedPredictedLocation(-1);
                Line2D line = new Line2D.Double(prevBulletPos.getX() - prevObjPos.getX(),
                                                prevBulletPos.getY() - prevObjPos.getY(),
                                                bullet.getCorrectedX() - obj.getCorrectedX(),
                                                bullet.getCorrectedY() - obj.getCorrectedY());
                if (SHOW_BOX.intersects(line.getBounds2D())) {
                  result.addMissLine(line, info.getIndex(), bullet, obj);
                }
              }
            }
          }
        }
      }
      return result;
    }

    protected abstract Collection<MovingGameObject> getInterestingObjects(FrameInfo info);
    protected abstract Collection<HitAnalysis.HitInfo> getHitInfos(HitAnalysis hitAnalysis);

  }

  private static final DataExtractor SMALL_ASTEROIDS_EXTRACTOR = new AbstractBasicDataExtractor()
  {
    protected Collection<HitAnalysis.HitInfo> getHitInfos(HitAnalysis hitAnalysis)
    {
      return hitAnalysis.getSmallAsteroidsInfo();
    }

    protected Collection<MovingGameObject> getInterestingObjects(FrameInfo info)
    {
      Collection<MovingGameObject> result = new LinkedList<MovingGameObject>();
      for (Asteroid ast: info.getAsteroids()) {
        if (ast.getScore() == Asteroid.SCORE_SMALL_ASTEROID) {
          result.add(ast);
        }
      }
      return result;
    }

    public int getSize()
    {
      return 8;
    }

    @Override
    public String toString()
    {
      return "Small Asteroids";
    }
  };

  private static final DataExtractor MIDDLE_ASTEROIDS_EXTRACTOR = new AbstractBasicDataExtractor()
  {
    protected Collection<HitAnalysis.HitInfo> getHitInfos(HitAnalysis hitAnalysis)
    {
      return hitAnalysis.getMiddleAsteroidsInfo();
    }

    protected Collection<MovingGameObject> getInterestingObjects(FrameInfo info)
    {
      Collection<MovingGameObject> result = new LinkedList<MovingGameObject>();
      for (Asteroid ast: info.getAsteroids()) {
        if (ast.getScore() == Asteroid.SCORE_MIDDLE_ASTEROID) {
          result.add(ast);
        }
      }
      return result;
    }

    public int getSize()
    {
      return 16;
    }

    @Override
    public String toString()
    {
      return "Middle Asteroids";
    }
  };

  private static final DataExtractor LARGE_ASTEROIDS_EXTRACTOR = new AbstractBasicDataExtractor()
  {
    protected Collection<HitAnalysis.HitInfo> getHitInfos(HitAnalysis hitAnalysis)
    {
      return hitAnalysis.getLargeAsteroidsInfo();
    }

    protected Collection<MovingGameObject> getInterestingObjects(FrameInfo info)
    {
      Collection<MovingGameObject> result = new LinkedList<MovingGameObject>();
      for (Asteroid ast: info.getAsteroids()) {
        if (ast.getScore() == Asteroid.SCORE_LARGE_ASTEROID) {
          result.add(ast);
        }
      }
      return result;
    }

    public int getSize()
    {
      return 32;
    }

    @Override
    public String toString()
    {
      return "Large Asteroids";
    }
  };

  private static final DataExtractor SMALL_UFOS_EXTRACTOR = new AbstractBasicDataExtractor()
  {
    protected Collection<HitAnalysis.HitInfo> getHitInfos(HitAnalysis hitAnalysis)
    {
      return hitAnalysis.getSmallUfosInfo();
    }

    protected Collection<MovingGameObject> getInterestingObjects(FrameInfo info)
    {
      Collection<MovingGameObject> result = new LinkedList<MovingGameObject>();
      Ufo ufo = info.getUfo();
      if (ufo != null  &&  ufo.getScore() == Ufo.SCORE_SMALL_UFO) {
        result.add(ufo);
      }
      return result;
    }

    public int getSize()
    {
      return Ufo.SMALL_SIZE;
    }

    @Override
    public String toString()
    {
      return "Small Ufos";
    }
  };

  private static final DataExtractor LARGE_UFOS_EXTRACTOR = new AbstractBasicDataExtractor()
  {
    protected Collection<HitAnalysis.HitInfo> getHitInfos(HitAnalysis hitAnalysis)
    {
      return hitAnalysis.getLargeUfosInfo();
    }

    protected Collection<MovingGameObject> getInterestingObjects(FrameInfo info)
    {
      Collection<MovingGameObject> result = new LinkedList<MovingGameObject>();
      Ufo ufo = info.getUfo();
      if (ufo != null  &&  ufo.getScore() == Ufo.SCORE_BIG_UFO) {
        result.add(ufo);
      }
      return result;
    }

    public int getSize()
    {
      return Ufo.BIG_SIZE;
    }

    @Override
    public String toString()
    {
      return "Large Ufos";
    }
  };

  private static final DataExtractor SHIPS_EXTRACTOR = new AbstractBasicDataExtractor()
  {
    protected Collection<HitAnalysis.HitInfo> getHitInfos(HitAnalysis hitAnalysis)
    {
      return hitAnalysis.getShipsInfo();
    }

    protected Collection<MovingGameObject> getInterestingObjects(FrameInfo info)
    {
      Collection<MovingGameObject> result = new LinkedList<MovingGameObject>();
      SpaceShip ship = info.getSpaceShip();
      if (ship != null) {
        result.add(ship);
      }
      return result;
    }

    public int getSize()
    {
      return 13;
    }

    @Override
    public String toString()
    {
      return "Ships";
    }
  };

  private static class AnalyseAction extends AbstractAction
  {
    private final HitAnalysisDisplay display;
    private final DataExtractor dataExtractor;

    AnalyseAction(HitAnalysisDisplay display, DataExtractor dataExtractor)
    {
      super(dataExtractor.toString());
      this.display       = display;
      this.dataExtractor = dataExtractor;
    }

    public void actionPerformed(ActionEvent e)
    {
      display.setExtractor(dataExtractor);
    }

    @Override
    protected Object clone() throws CloneNotSupportedException
    {
      return super.clone();
    }
  }

  private class ShapePainterAction extends AbstractAction
  {
    private final ShapePainter shapePainter;

    ShapePainterAction(String name, ShapePainter shapePainter)
    {
      super(name);
      this.shapePainter = shapePainter;
    }

    public void actionPerformed(ActionEvent e)
    {
      painter = shapePainter;
      refresh();
    }

    @Override
    protected Object clone() throws CloneNotSupportedException
    {
      return super.clone();
    }
  }

  private class ShowSizeAction extends AbstractAction
  {
    private final int size;

    ShowSizeAction(int size)
    {
      super(String.format("%dx%d", 2*size, 2*size));
      this.size = size;
    }

    public void actionPerformed(ActionEvent e)
    {
      showSize = size;
      refresh();
    }

    @Override
    protected Object clone() throws CloneNotSupportedException
    {
      return super.clone();
    }
  }

  private class ChangeSizeAction extends AbstractAction
  {
    private final int delta;

    private ChangeSizeAction(String name, int delta)
    {
      super(name);
      this.delta = delta;
    }

    public void actionPerformed(ActionEvent e)
    {
      deltaR += delta;
      if (deltaR < -4) {
        deltaR = -4;
      }
      refresh();
    }

    @Override
    protected Object clone() throws CloneNotSupportedException
    {
      return super.clone();
    }
  }

  private class DeltaAction extends AbstractAction
  {
    private final Point delta;

    DeltaAction(String name, int dx, int dy)
    {
      super(name);
      delta = new Point(dx, dy);
    }

    public void actionPerformed(ActionEvent e)
    {
      deltaX += delta.x;
      deltaY += delta.y;
      refresh();
    }

    @Override
    protected Object clone() throws CloneNotSupportedException
    {
      return super.clone();
    }
  }

  private DumpFile    dumpFile;
  private HitAnalysis analysis;
  private HitAndMiss  hitAndMiss;
  private JFrame      frame;
  private DataExtractor extractor;
  private ShapePainter painter = new NullShapePainter();
  private int          showSize = MAX_SHOW_SIZE/2;
  private int          deltaX = 0;
  private int          deltaY = 0;
  private int          deltaR = 0;

  public HitAnalysisDisplay(JFrame frame, String dump) throws IOException
  {
    this.frame = frame;
    dumpFile = new DumpFile(dump);
    dumpFile.runPreparerDirectly(new ImprovedVelocityPreparer());
    analysis = new HitAnalysis();
    analysis.analyse(dumpFile.getInfos());
    setExtractor(SHIPS_EXTRACTOR);  // usually the fastest
    setPreferredSize(new Dimension(500, 500));
    final JPopupMenu popupMenu = new JPopupMenu("Select Objects");
    popupMenu.add(new AnalyseAction(this, SMALL_ASTEROIDS_EXTRACTOR));
    popupMenu.add(new AnalyseAction(this, MIDDLE_ASTEROIDS_EXTRACTOR));
    popupMenu.add(new AnalyseAction(this, LARGE_ASTEROIDS_EXTRACTOR));
    popupMenu.add(new AnalyseAction(this, SMALL_UFOS_EXTRACTOR));
    popupMenu.add(new AnalyseAction(this, LARGE_UFOS_EXTRACTOR));
    popupMenu.add(new AnalyseAction(this, SHIPS_EXTRACTOR));
    popupMenu.addSeparator();
    popupMenu.add(new ShowSizeAction(MAX_SHOW_SIZE));
    popupMenu.add(new ShowSizeAction(MAX_SHOW_SIZE/2));
    popupMenu.add(new ShowSizeAction(MAX_SHOW_SIZE/4));
    popupMenu.add(new ShowSizeAction(MAX_SHOW_SIZE/8));
    popupMenu.add(new ShowSizeAction(MAX_SHOW_SIZE/16));
    popupMenu.addSeparator();
    popupMenu.add(new ShapePainterAction("No Shape",  new NullShapePainter()));
    popupMenu.add(new ShapePainterAction("Rectangle", new RectangularShapePainter()));
    popupMenu.add(new ShapePainterAction("Circle",    new CircularShapePainter()));
    popupMenu.add(new ShapePainterAction("Octagon",   new OctagonalShapePainter()));
    popupMenu.addSeparator();
    popupMenu.add(new DeltaAction("Shape left", -1,  0));
    popupMenu.add(new DeltaAction("Shape right", 1,  0));
    popupMenu.add(new DeltaAction("Shape up",    0,  1));
    popupMenu.add(new DeltaAction("Shape down",  0, -1));
    popupMenu.addSeparator();
    popupMenu.add(new ChangeSizeAction("Shrink Shape", -1));
    popupMenu.add(new ChangeSizeAction("Expand Shape",  1));

    addMouseListener(new MouseAdapter()
    {
      @Override
      public void mouseClicked(MouseEvent e)
      {
        if (e.isPopupTrigger()) {
          return;
        }
        Collection<LineInfo> hits = new LinkedList<LineInfo>();
        Rectangle2D hitArea = new Rectangle2D.Double(e.getX() - HIT_RADIUS,
                                                     e.getY() - HIT_RADIUS,
                                                     2*HIT_RADIUS, 2*HIT_RADIUS);
        int smaller = Math.min(getWidth(), getHeight());
        double scaling = smaller / (2*showSize);
        AffineTransform backTrafo = AffineTransform.getScaleInstance(1/scaling, -1/scaling);
        backTrafo.concatenate(AffineTransform.getTranslateInstance(-getWidth()/2, -getHeight()/2));
        hitArea = backTrafo.createTransformedShape(hitArea).getBounds2D();
        for (LineInfo lineInfo: hitAndMiss.getHitLines()) {
          if (hitArea.intersectsLine(lineInfo.getLine())) {
            hits.add(lineInfo);
          }
        }
        for (LineInfo lineInfo: hitAndMiss.getMissLines()) {
          if (hitArea.intersectsLine(lineInfo.getLine())) {
            hits.add(lineInfo);
          }
        }
        for (LineInfo info: hits) {
          System.out.println(info);
        }
        System.out.println();
      }

      @Override
      public void mousePressed(MouseEvent e)
      {
        if (e.isPopupTrigger()) {
          popupMenu.show(e.getComponent(), e.getX(), e.getY());
        }
      }
    });
  }

  private void setExtractor(DataExtractor extractor)
  {
    this.extractor = extractor;
    hitAndMiss = extractor.getHitAndMiss(dumpFile, analysis);
    refresh();
  }

  private void refresh()
  {
    refreshTitle();
    repaint();
  }

  private void refreshTitle()
  {
    frame.setTitle(String.format("Hit Analysis -- %s, [%dx%d], delta=[%d,%d;%d]",
                                 extractor.toString(),
                                 2*showSize, 2*showSize,
                                 deltaX, deltaY, deltaR));
  }

  /**
   * Paint the component.
   * @param g the <code>Graphics</code> object to protect
   * @see #paint
   * @see javax.swing.plaf.ComponentUI
   */
  @Override
  protected void paintComponent(Graphics g)
  {
    int smaller = Math.min(getWidth(), getHeight());
    if (smaller > 0) {
      g.setColor(Color.black);
      g.fillRect(0, 0, getWidth(), getHeight());
      Graphics2D g2 = (Graphics2D)g.create();
      double scaling = smaller / (2*showSize);
      g2.translate(getWidth()/2, getHeight()/2);
      g2.scale(scaling, -scaling);
      g2.setStroke(new BasicStroke(0));
      g2.setColor(HIT_COLOR);
      for (LineInfo lineInfo: hitAndMiss.getHitLines()) {
        g2.draw(lineInfo.getLine());
      }
      g2.setColor(MISS_COLOR);
      for (LineInfo lineInfo: hitAndMiss.getMissLines()) {
        g2.draw(lineInfo.getLine());
      }
      painter.drawShape(g2);
    }
  }

  public static void main(String[] args) throws IOException
  {
    if (args.length > 0) {
      JFrame frame = new JFrame("Hit Analysis");
      HitAnalysisDisplay display = new HitAnalysisDisplay(frame, args[0]);
      frame.getContentPane().add(display, BorderLayout.CENTER);
      frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
      frame.pack();
      frame.setVisible(true);
    }
  }
}
