/*
Copyright 2003-2012 Dmitry Barashev, GanttProject Team

This file is part of GanttProject, an opensource project management tool.

GanttProject is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
 the Free Software Foundation, either version 3 of the License, or
 (at your option) any later version.

GanttProject is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with GanttProject.  If not, see <http://www.gnu.org/licenses/>.
 */
package biz.ganttproject.core.chart.canvas;

import java.awt.Color;
import java.awt.Font;
import java.awt.Paint;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;



/**
 * Stores the available primitives and their information (used for painting) and
 * provides methods to retrieve them
 *
 * @author bard
 */
public class Canvas {
  private ArrayList<Rectangle> myRectangles = new ArrayList<Rectangle>();

  private ArrayList<Line> myLines = new ArrayList<Line>();

  private ArrayList<Text> myTexts = new ArrayList<Text>();

  private Map<Object, Shape> myModelObject2primitive = new WeakHashMap<Object, Shape>();

  private List<Canvas> myLayers = new ArrayList<Canvas>();

  private int myDeltaX;

  private int myDeltaY;

  private List<TextGroup> myTextGroups = new ArrayList<TextGroup>();

  private final DummySpatialIndex<Text> myTextIndex = new DummySpatialIndex<Text>();

  private final DummySpatialIndex<Polygon> myPolygonIndex = new DummySpatialIndex<Canvas.Polygon>();
  
  /** Horizontal alignments for texts */
  public enum HAlignment {
    CENTER, LEFT, RIGHT
  };

  /** Vertical alignments for texts */
  public enum VAlignment {
    CENTER, TOP, BOTTOM
  };

  public static class Shape {
    private Color myBackgroundColor;

    private Color myForegroundColor;

    private String myStyleName;

    private Object myModelObject;

    private boolean isVisible = true;

    private LinkedHashSet<String> myStyles;

    private Float myOpacity = null;

    private LinkedHashSet<String> getStyles() {
      if (myStyles == null) {
        myStyles = new LinkedHashSet<String>();
      }
      return myStyles;
    }

    public void addStyle(String style) {
      getStyles().add(style);
    }

    public boolean hasStyle(String style) {
      return getStyles().contains(style);
    }

    public void setStyle(String styleName) {
      myStyleName = styleName;
    }

    public String getStyle() {
      return myStyleName;
    }

    public Color getBackgroundColor() {
      return myBackgroundColor;
    }

    public void setBackgroundColor(Color myBackgroundColor) {
      this.myBackgroundColor = myBackgroundColor;
    }

    public Color getForegroundColor() {
      return myForegroundColor;
    }

    public void setForegroundColor(Color myForegroundColor) {
      this.myForegroundColor = myForegroundColor;
    }

    public Object getModelObject() {
      return myModelObject;
    }

    public void setModelObject(Object modelObject) {
      myModelObject = modelObject;
    }

    public boolean isVisible() {
      return isVisible;
    }

    public void setVisible(boolean visible) {
      isVisible = visible;
    }
    
    public Float getOpacity() {
      return myOpacity;
    }
    
    public void setOpacity(float opacity) {
      myOpacity = opacity;
    }
  }

  public static class Polygon extends Shape {
    private final int myLeftX;
    private final int myRightX;
    private final int myTopY;
    private final int myBottomY;
    
    private final int[] myPointsX;
    private final int[] myPointsY;
    
    private Polygon(int... points) {
      myPointsX = new int[points.length / 2];
      myPointsY = new int[points.length / 2];
      
      int leftX = Integer.MAX_VALUE;
      int topY = Integer.MAX_VALUE;
      int rightX = Integer.MIN_VALUE;
      int bottomY = Integer.MIN_VALUE;
      for (int i = 0; i < points.length / 2; i++) {
        leftX = Math.min(leftX, points[i * 2]);
        rightX = Math.max(rightX, points[i * 2]);
        topY = Math.min(topY, points[i * 2 + 1]);
        bottomY = Math.max(bottomY, points[i * 2 + 1]);
        
        myPointsX[i] = points[i * 2];
        myPointsY[i] = points[i * 2 + 1];
      }
      myLeftX = leftX;
      myRightX = rightX;
      myTopY = topY;
      myBottomY = bottomY;
    }
    
    public int[] getPointsX() {
      return myPointsX;
    }
    
    public int[] getPointsY() {
      return myPointsY;
    }
    
    public int getPointCount() {
      return myPointsX.length;
    }
    
    public int getLeftX() {
      return myLeftX;
    }
    
    public int getTopY() {
      return myTopY;
    }
    
    public int getRightX() {
      return myRightX;
    }
    
    public int getBottomY() {
      return myBottomY;
    }

    public int getHeight() {
      return myBottomY - myTopY;
    }

    public int getWidth() {
      return myRightX - myLeftX;
    }
    
    public int getMiddleY() {
      return myTopY + getHeight() / 2;
    }

    public int getMiddleX() {
      return myLeftX + getWidth() / 2;
    }

    @Override
    public String toString() {
      return "Polygon[" + myLeftX + "," + myTopY + ",w=" + getWidth() + ",h=" + getHeight() + ",style=" + getStyle() + "]";
    }
  }
  
  public static class Rectangle extends Polygon {
    public Paint myPaint;
    
    private Rectangle(int leftx, int topy, int width, int height) {
      super(leftx, topy, leftx + width, topy + height);
    }

    public Paint getBackgroundPaint() {
      return myPaint;
    }
    
    public void setBackgroundPaint(Paint paint) {
      myPaint = paint;
    }
  }

  public static class Line extends Shape {
    public static enum Arrow { NONE, START, FINISH }

    private final int myStartX;

    private final int myStartY;

    private final int myFinishX;

    private final int myFinishY;

    private Arrow myArrow = Arrow.NONE;

    private Line(int startx, int starty, int finishx, int finishy) {
      myStartX = startx;
      myStartY = starty;
      myFinishX = finishx;
      myFinishY = finishy;
    }

    public int getStartX() {
      return myStartX;
    }

    public int getStartY() {
      return myStartY;
    }

    public int getFinishX() {
      return myFinishX;
    }

    public int getFinishY() {
      return myFinishY;
    }

    public void setArrow(Arrow arrow) {
      myArrow = arrow;
    }

    public Arrow getArrow() {
      return myArrow;
    }
  }

  public static class Label {
    public final String text;
    public final int lengthPx;
    public final int heightPx;
    private final Text myOwner;

    public Label(Text owner, String text, int lengthPx) {
      this(owner, text, lengthPx, Integer.MIN_VALUE);
    }

    public Label(Text owner, String text, int lengthPx, int heightPx) {
      this.myOwner = owner;
      this.text = text;
      this.lengthPx = lengthPx;
      this.heightPx = heightPx;
    }

    public void setVisible(boolean isVisible) {
      if (isVisible && myOwner != null) {
        myOwner.index(this);
      }
    }
  }

  public static class Text extends Shape {
    private final int myLeftX;

    private final int myBottomY;

    private Font myFont;

    private int myMaxLength;

    private HAlignment myHAlignment = HAlignment.LEFT;

    private VAlignment myVAlignment = VAlignment.BOTTOM;

    private TextSelector mySelector;

    private final SpatialIndex<Text> myIndex;

    private Text(int leftX, int bottomY, final String text, SpatialIndex<Text> index) {
      this(leftX, bottomY, (TextSelector)null, index);
      mySelector = new TextSelector() {
        @Override
        public Label[] getLabels(TextMetrics textLengthCalculator) {
          return new Label[] {createLabel(text, textLengthCalculator.getTextLength(text))};
        }
      };
    }

    void index(Label label) {
      assert label.myOwner == this;
      if (myIndex != null && label.heightPx != Integer.MIN_VALUE) {
        myIndex.put(this, myLeftX, myBottomY, label.lengthPx, label.heightPx);
      }
    }

    private Text(int leftX, int bottomY, TextSelector delegateSelector) {
      this(leftX, bottomY, delegateSelector, null);
    }

    private Text(int leftX, int bottomY, TextSelector delegateSelector, SpatialIndex<Text> index) {
      myLeftX = leftX;
      myBottomY = bottomY;
      mySelector = delegateSelector;
      myMaxLength = -1;
      myIndex = index;
    }

    public void setFont(Font font) {
      myFont = font;
    }

    public void setMaxLength(int maxLength) {
      myMaxLength = maxLength;
    }

    public int getMaxLength() {
      return myMaxLength;
    }

    public Font getFont() {
      return myFont;
    }

    public Label[] getLabels(TextMetrics textLengthCalculator) {
      return mySelector.getLabels(textLengthCalculator);
      // String text = mySelector.getText(textLengthCalculator);
      // int length = textLengthCalculator.getTextLength(text);
      // return new Label[] { new Label(text, length) };
    }

    public int getLeftX() {
      return myLeftX;
    }

    public int getBottomY() {
      return myBottomY;
    }

    public void setAlignment(HAlignment halignment, VAlignment valignment) {
      myHAlignment = halignment;
      myVAlignment = valignment;
    }

    public HAlignment getHAlignment() {
      return myHAlignment;
    }

    public VAlignment getVAlignment() {
      return myVAlignment;
    }

    public TextSelector getTextSelector() {
      return mySelector;
    }

    public Label createLabel(String text, int lengthPx) {
      return createLabel(text, lengthPx, Integer.MIN_VALUE);
    }

    public Label createLabel(String text, int lengthPx, int heightPx) {
      return new Label(this, text, lengthPx, heightPx);
    }

    @Override
    public String toString() {
      return String.format("TBox [%d, %d]", myLeftX, myBottomY);
    }

    public void setSelector(TextSelector selector) {
      mySelector = selector;
    }
  }

  public static class TextGroup {
    private List<String> myLineStyles;
    private int myHeight;
    private FontChooser myFontChooser;
    private List<Font> myFonts;
    private List<List<Text>> myLines = new ArrayList<List<Text>>();
    private int myBottomY;
    private int myLeftX;
    private List<Integer> myBaselines = new ArrayList<Integer>();

    public TextGroup(int leftX, int bottomY, int height, String... lineStyles) {
      myLeftX = leftX;
      myBottomY = bottomY;
      myHeight = height;
      myLineStyles = new ArrayList<String>(Arrays.asList(lineStyles));
      for (int i = 0; i < myLineStyles.size(); i++) {
        myLines.add(new ArrayList<Text>());
      }
      myFonts = new ArrayList<Font>();
    }

    public void setFonts(FontChooser fontChooser) {
      for (int totalHeight = getTotalHeight(fontChooser, myBaselines); totalHeight > myHeight;
          totalHeight = getTotalHeight(fontChooser, myBaselines)) {
        int baseSize = fontChooser.decreaseBaseFontSize();
        if (baseSize < 8) {
          break;
        }
      }
      for (String style : myLineStyles) {
        myFonts.add(fontChooser.getFont(style));
      }
      myFontChooser = fontChooser;
    }

    private int getTotalHeight(FontChooser fontChooser, List<Integer> lineBaselines) {
      lineBaselines.clear();
      int totalHeight = 0;
      for (String style : myLineStyles) {
        totalHeight += fontChooser.getMarginTop(style);
        totalHeight += fontChooser.getTextHeight(style);
        totalHeight += fontChooser.getMarginBottom(style);
        lineBaselines.add(totalHeight);
      }
      return totalHeight;
    }

    public Text addText(int x, int y, TextSelector textSelector) {
      Text result = new Text(x, y, textSelector);
      myLines.get(y).add(result);
      return result;
    }

    public int getLineCount() {
      return myLines.size();
    }

    public List<Text> getLine(int i) {
      return myLines.get(i);
    }

    public Font getFont(int i) {
      return myFonts.get(i);
    }

    public Color getColor(int i) {
      return myFontChooser.getColor(myLineStyles.get(i));
    }

    public int getLeftX() {
      return myLeftX;
    }

    public int getBottomY() {
      return myBottomY;
    }

    public int getBottomY(int line) {
      return myBottomY + myBaselines.get(line);
    }
  }

  public Canvas() {
    this(0, 0);
  }

  public Canvas(int deltax, int deltay) {
    myDeltaX = deltax;
    myDeltaY = deltay;
  }

  public void setOffset(int deltax, int deltay) {
    myDeltaX = deltax;
    myDeltaY = deltay;
    // for (GraphicPrimitiveContainer layer : myLayers) {
    // layer.setOffset(deltax, deltay);
    // }
  }

  public Polygon createPolygon(int... points) {
    assert points.length % 2 == 0 : "The number of points must be even";
    Polygon result = new Polygon(points);
    myPolygonIndex.put(result, result.getLeftX(), result.getBottomY(), result.getWidth(), result.getHeight());
    return result;
  }
  
  public Rectangle createRectangle(int leftx, int topy, int width, int height) {
    Rectangle result = createDetachedRectangle(leftx, topy, width, height);
    myRectangles.add(result);
    return result;
  }

  public Rectangle createDetachedRectangle(int leftx, int topy, int width, int height) {
    if (width < 0) {
      width = -width;
      leftx = leftx - width;
    }
    return new Rectangle(leftx + myDeltaX, topy + myDeltaY, width, height);
  }
  
  public Line createLine(int startx, int starty, int finishx, int finishy) {
    Line result = new Line(startx + myDeltaX, starty + myDeltaY, finishx + myDeltaX, finishy + myDeltaY);
    myLines.add(result);
    return result;
  }

  public Text createText(int leftx, int bottomy, String text) {
    Text result = new Text(leftx + myDeltaX, bottomy + myDeltaY, text, myTextIndex);
    myTexts.add(result);
    return result;
  }

  public Text createText(int leftx, int bottomy, TextSelector textSelector) {
    Text result = new Text(leftx + myDeltaX, bottomy + myDeltaY, textSelector, myTextIndex);
    myTexts.add(result);
    return result;
  }

  public TextGroup createTextGroup(int leftX, int bottomY, int height, String... styles) {
    TextGroup result = new TextGroup(leftX, bottomY, height, styles);
    myTextGroups.add(result);
    return result;
  }

  public void paint(Painter painter) {
    painter.prePaint();
    for (int i = 0; i < myRectangles.size(); i++) {
      Rectangle next = myRectangles.get(i);
      if (next.isVisible()) {
        painter.paint(next);
      }
    }
    for (Polygon p : myPolygonIndex.values()) {
      if (p.isVisible()) {
        painter.paint(p);
      }
    }
    for (int i = 0; i < myLines.size(); i++) {
      Line next = myLines.get(i);
      if (next.isVisible()) {
        painter.paint(next);
      }
    }
    for (int i = 0; i < myTexts.size(); i++) {
      Text next = myTexts.get(i);
      if (next.isVisible()) {
        painter.paint(next);
      }
    }
    for (TextGroup textGroup : myTextGroups) {
      painter.paint(textGroup);
    }
  }

  public void clear() {
    myTextIndex.clear();
    myPolygonIndex.clear();
    myRectangles.clear();
    myLines.clear();
    myTexts.clear();
    myTextGroups.clear();
    myModelObject2primitive.clear();
    for (Canvas layer : getLayers()) {
      layer.clear();
    }
  }

  public void bind(Shape primitive, Object modelObject) {
    myModelObject2primitive.put(modelObject, primitive);
    primitive.setModelObject(modelObject);
  }

  public Shape getPrimitive(Object modelObject) {
    return myModelObject2primitive.get(modelObject);
  }

  public Shape getPrimitive(int x, int y) {
    return getPrimitive(x, 0, y, 0);
  }

  public Shape getPrimitive(int x, int xThreshold, int y, int yThreshold) {
    Shape result = null;
    for (int i = 0; i < myRectangles.size(); i++) {
      Rectangle next = myRectangles.get(i);
      // System.err.println(" next rectangle="+next);
      if (next.getLeftX() <= x + xThreshold && next.getRightX() >= x - xThreshold
          && next.getTopY() <= y + yThreshold && next.getBottomY() >= y - yThreshold) {
        result = next;
        break;
      }
    }
    if (result != null) {
      return result;
    }
    result = myPolygonIndex.get(x, xThreshold, y, yThreshold);
    if (result != null) {
      return result;
    }
    return myTextIndex.get(x + myDeltaX, y + myDeltaY);
  }

  public List<Canvas> getLayers() {
    return Collections.unmodifiableList(myLayers);
  }

  public Canvas getLayer(int layer) {
    if (layer < 0 || layer >= myLayers.size()) {
      throw new IllegalArgumentException();
    }
    return myLayers.get(layer);
  }

  public Canvas newLayer() {
    Canvas result = new Canvas();
    myLayers.add(result);
    return result;
  }

  public void createLayers(int count) {
    for (int i = 0; i < count; i++) {
      newLayer();
    }
  }
}
