/**
 * Chart2D, a java library for drawing two dimensional charts.
 * Copyright (C) 2001 Jason J. Simas
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

package net.sourceforge.chart2d;


import java.awt.*;
import java.awt.geom.*;


/**
 * Shared layout engine for labels-bottom and labels-left graph areas.
 */
final class GraphLayoutEngine {

  static final class LayoutResult {
    final FancyBar[][] bars;
    final FancyDot[][] dots;
    final FancyLine[] lines;

    LayoutResult (FancyBar[][] b, FancyDot[][] d, FancyLine[] l) {
      bars = b;
      dots = d;
      lines = l;
    }
  }


  private static final class LayoutContext {
    boolean labelsBottom;
    boolean allowAlignment;
    boolean valid;
    int[][] graphLengths;
    int[][] barLowValues;
    Rectangle[] categoryTicks;
    int numSets;
    int numCats;
    int numCompsPerCat;
    int numBars;
    int numDots;
    int numLinePoints;
    int barsThickness;
    int dotsThickness;
    int linesThickness;
    int componentsThickness;
    int setsThickness;
    int domainStart;
    int domainSize;
    Rectangle2D.Float boundsGraph;
  }


  static LayoutResult layout (GraphArea area) {

    LayoutContext context = createLayoutContext (area);
    if (!context.valid) {
      return new LayoutResult (
        new FancyBar[0][0], new FancyDot[0][0], new FancyLine[0]);
    }
    FancyBar[][] bars = buildBars (area, context);
    FancyDot[][] dots = buildDots (area, context);
    FancyLine[] lines = buildLines (area, context);
    return new LayoutResult (bars, dots, lines);
  }


  private static LayoutContext createLayoutContext (GraphArea area) {

    LayoutContext context = new LayoutContext();
    context.labelsBottom = area.getType() == FancyShape.LABELSBOTTOM;
    context.allowAlignment = area.getAllowComponentAlignment();
    context.graphLengths = area.getGraphValues();
    context.barLowValues = area.getBarLowValues();
    context.numSets = context.graphLengths.length;
    context.categoryTicks =
      context.labelsBottom ? area.getXTicks() : area.getYTicks();

    if (context.numSets > 0) {
      context.numCats = area.getLabelsAxisTicksAlignment() == Area.BETWEEN ?
        context.categoryTicks.length + 1 : context.categoryTicks.length;
      context.numBars = area.getBarsExistence() ? context.numCats : 0;
      context.numDots = area.getDotsExistence() ? context.numCats : 0;
      context.numLinePoints = area.getLinesExistence() ? context.numCats : 0;
      context.numCompsPerCat = context.numCats > 0 ?
        (int)(context.graphLengths[0].length / context.numCats) : 0;
    }

    context.valid =
      context.numSets > 0 && context.numCats > 0 && context.numCompsPerCat > 0;
    if (!context.valid) return context;

    float ratio = area.getRatio (
      context.labelsBottom ? Area.WIDTH : Area.HEIGHT);
    int availableThickness =
      context.labelsBottom ? area.getSpaceSize(Area.MIN).width :
      area.getSpaceSize(Area.MIN).height;
    int numGaps = area.getBetweenComponentsGapExistence() ? context.numCats : 0;
    if (numGaps > 0) {
      int gapThickness =
        area.applyRatio (area.getBetweenComponentsGapThicknessModel(), ratio);
      gapThickness =
        numGaps * gapThickness <= availableThickness ?
        gapThickness : availableThickness / numGaps;
      availableThickness -= numGaps * gapThickness;
    }

    context.barsThickness = context.numBars > 0 ? computeThickness (
      availableThickness, context.numSets, context.numBars,
      context.numCompsPerCat,
      area.applyRatio (area.getBarsThicknessModel(), ratio),
      area.getBarsExcessSpaceFeedbackRatio(),
      area.getBarsWithinCategoryOverlapRatio()) : 0;
    context.dotsThickness = context.numDots > 0 ? computeThickness (
      availableThickness, context.numSets, context.numDots,
      context.numCompsPerCat,
      area.applyRatio (area.getDotsThicknessModel(), area.getRatio (Area.LESSER)),
      area.getDotsExcessSpaceFeedbackRatio(),
      area.getDotsWithinCategoryOverlapRatio()) : 0;
    context.linesThickness = context.numLinePoints > 0 ? computeThickness (
      availableThickness, context.numSets, context.numLinePoints,
      context.numCompsPerCat,
      area.applyRatio (area.getLinesThicknessModel(), area.getRatio (Area.LESSER)),
      area.getLinesExcessSpaceFeedbackRatio(),
      area.getLinesWithinCategoryOverlapRatio()) : 0;

    context.componentsThickness = context.barsThickness;
    context.componentsThickness = context.dotsThickness > context.componentsThickness ?
      context.dotsThickness : context.componentsThickness;
    context.componentsThickness = context.linesThickness > context.componentsThickness ?
      context.linesThickness : context.componentsThickness;
    context.setsThickness = context.componentsThickness * context.numSets;
    int maxSetsThickness =
      availableThickness / (context.numCats * context.numCompsPerCat);
    context.setsThickness =
      context.setsThickness > maxSetsThickness ?
      maxSetsThickness : context.setsThickness;
    context.componentsThickness =
      context.numSets > 0 ? context.setsThickness / context.numSets : 0;

    context.boundsGraph = new Rectangle2D.Float (
      area.getSpaceSizeLocation (Area.MIN).x,
      area.getSpaceSizeLocation (Area.MIN).y,
      area.getSpaceSize (Area.MIN).width,
      area.getSpaceSize (Area.MIN).height);
    context.domainStart =
      context.labelsBottom ? area.getSpaceSizeLocation (Area.MIN).x :
      area.getSpaceSizeLocation (Area.MIN).y;
    context.domainSize =
      context.labelsBottom ? area.getSpaceSize (Area.MIN).width :
      area.getSpaceSize (Area.MIN).height;

    return context;
  }


  private static int computeThickness (
    int availableThickness, int numSets, int numCats, int numCompsPerCat,
    int thickness, float feedbackRatio, float overlapRatio) {

    if ((numSets * numCats * numCompsPerCat) * thickness <=
      availableThickness) {
      float leftover =
        (availableThickness - thickness * (numSets * numCats * numCompsPerCat)) /
        (float)(numSets * numCats * numCompsPerCat);
      return thickness + (int)(feedbackRatio * leftover);
    }

    float numOverlapCompsPerCat =
      (1 + (1 - overlapRatio) * (numCompsPerCat - 1));
    if (numSets * numCats * numOverlapCompsPerCat * thickness >
      availableThickness) {
      return (int)(availableThickness /
        (numSets * numCats * numOverlapCompsPerCat));
    }
    return thickness;
  }


  private static FancyBar[][] buildBars (GraphArea area, LayoutContext context) {

    FancyBar[][] bars =
      new FancyBar[context.numSets][context.numBars * context.numCompsPerCat];
    if (context.numBars == 0) return bars;

    float catDelta =
      context.domainSize / (float)(context.numBars * context.numCompsPerCat);
    int domainSlotThickness =
      context.allowAlignment ? context.barsThickness : context.setsThickness;
    int crossValueBase = context.labelsBottom ?
      area.getSpaceSizeLocation (Area.MIN).y + area.getSpaceSize (Area.MIN).height :
      area.getSpaceSizeLocation (Area.MIN).x +
      (context.allowAlignment ? 0 : (area.getOutlineComponents() ? -1 : 0));
    int boundValueBase = context.labelsBottom ?
      crossValueBase :
      area.getSpaceSizeLocation (Area.MIN).x + (area.getOutlineComponents() ? -1 : 0);

    if (context.numBars == 1) {
      int start = getSingleCategoryStart (
        context, domainSlotThickness, catDelta);
      populateBarsForCategory (
        area, context, bars, 0, start, catDelta, crossValueBase, boundValueBase);
    }
    else {
      float tickHalf = getTickHalfSize (
        context.categoryTicks[0], context.labelsBottom);
      float minDomain = context.domainStart;
      float maxDomain = getTickPosition (
        context.categoryTicks[0], context.labelsBottom) + tickHalf;
      for (int i = 0; i < context.numBars; ++i) {
        int start = getCategoryStart (
          area, context, minDomain, maxDomain, domainSlotThickness, catDelta);
        populateBarsForCategory (
          area, context, bars, i, start, catDelta, crossValueBase, boundValueBase);
        if (i + 1 == context.numBars) break;
        minDomain = maxDomain;
        if (i + 2 == context.numBars &&
          area.getLabelsAxisTicksAlignment() == Area.BETWEEN) {
          maxDomain = context.domainStart + context.domainSize;
        }
        else {
          maxDomain = getTickPosition (
            context.categoryTicks[i + 1], context.labelsBottom) + tickHalf;
        }
      }
    }
    return bars;
  }


  private static void populateBarsForCategory (
    GraphArea area, LayoutContext context, FancyBar[][] bars, int categoryIndex,
    int categoryStart, float catDelta, int crossValueBase, int boundValueBase) {

    for (int k = 0; k < context.numCompsPerCat; ++k) {
      for (int j = 0; j < context.numSets; ++j) {
        int item = categoryIndex * context.numCompsPerCat + k;
        float domainPos = categoryStart + k * catDelta;
        if (!context.allowAlignment) {
          domainPos += j * context.componentsThickness +
            (context.componentsThickness - context.barsThickness) / 2f;
        }
        FancyBar bar = new FancyBar();
        bars[j][item] = bar;
        bar.setDataSign (area.getDataSign());
        bar.setBaseValue (
          context.labelsBottom ?
          crossValueBase - area.getLinesFillInteriorBaseValue() :
          crossValueBase + area.getLinesFillInteriorBaseValue());

        int value = context.graphLengths[j][item];
        int lowValue = context.barLowValues[j][item];
        if (context.labelsBottom) {
          if (value > lowValue) {
            bar.setBounds (new Rectangle2D.Float (
              domainPos, crossValueBase - value,
              context.barsThickness, value - lowValue));
          }
          else {
            bar.setBounds (new Rectangle2D.Float (
              domainPos, crossValueBase - lowValue,
              context.barsThickness, lowValue - value));
          }
          Rectangle2D.Float clipBounds = new Rectangle2D.Float (
            context.boundsGraph.x, bar.getBounds().y,
            context.boundsGraph.width, bar.getBounds().height);
          area.configureBarShape (
            bar, context.boundsGraph, clipBounds,
            !area.getComponentsColoringByCat() ?
            area.getBarColors()[j] : area.getComponentsColorsByCat()[categoryIndex],
            context.barsThickness);
        }
        else {
          if (value > lowValue) {
            bar.setBounds (new Rectangle2D.Float (
              boundValueBase + lowValue, domainPos,
              value - lowValue, context.barsThickness));
          }
          else {
            bar.setBounds (new Rectangle2D.Float (
              boundValueBase + value, domainPos,
              lowValue - value, context.barsThickness));
          }
          Rectangle2D.Float clipBounds = new Rectangle2D.Float (
            bar.getBounds().x, context.boundsGraph.y,
            bar.getBounds().width, context.boundsGraph.height);
          area.configureBarShape (
            bar, context.boundsGraph, clipBounds,
            !area.getComponentsColoringByCat() ?
            area.getBarColors()[j] : area.getComponentsColorsByCat()[categoryIndex],
            context.barsThickness);
        }
      }
    }
  }


  private static FancyDot[][] buildDots (GraphArea area, LayoutContext context) {

    FancyDot[][] dots =
      new FancyDot[context.numSets][context.numDots * context.numCompsPerCat];
    if (context.numDots == 0) return dots;

    float catDelta =
      context.domainSize / (float)(context.numDots * context.numCompsPerCat);
    int domainSlotThickness =
      context.allowAlignment ? context.dotsThickness : context.setsThickness;
    int valueBase = context.labelsBottom ?
      area.getSpaceSizeLocation (Area.MIN).y +
      area.getSpaceSize (Area.MIN).height - context.dotsThickness / 2 :
      area.getSpaceSizeLocation (Area.MIN).x - context.dotsThickness / 2;

    if (context.numDots == 1) {
      int start = getSingleCategoryStart (
        context, domainSlotThickness, catDelta);
      populateDotsForCategory (
        area, context, dots, 0, start, catDelta, valueBase);
    }
    else {
      float tickHalf = getTickHalfSize (
        context.categoryTicks[0], context.labelsBottom);
      float minDomain = context.domainStart;
      float maxDomain = getTickPosition (
        context.categoryTicks[0], context.labelsBottom) + tickHalf;
      for (int i = 0; i < context.numDots; ++i) {
        int start = getCategoryStart (
          area, context, minDomain, maxDomain, domainSlotThickness, catDelta);
        populateDotsForCategory (
          area, context, dots, i, start, catDelta, valueBase);
        if (i + 1 == context.numDots) break;
        minDomain = maxDomain;
        if (i + 2 == context.numDots &&
          area.getLabelsAxisTicksAlignment() == Area.BETWEEN) {
          maxDomain = context.domainStart + context.domainSize;
        }
        else {
          maxDomain = getTickPosition (
            context.categoryTicks[i + 1], context.labelsBottom) + tickHalf;
        }
      }
    }
    return dots;
  }


  private static void populateDotsForCategory (
    GraphArea area, LayoutContext context, FancyDot[][] dots, int categoryIndex,
    int categoryStart, float catDelta, int valueBase) {

    for (int k = 0; k < context.numCompsPerCat; ++k) {
      for (int j = 0; j < context.numSets; ++j) {
        int item = categoryIndex * context.numCompsPerCat + k;
        float domainPos = categoryStart + k * catDelta;
        if (!context.allowAlignment) {
          domainPos += j * context.componentsThickness +
            (context.componentsThickness - context.dotsThickness) / 2f;
        }
        FancyDot dot = new FancyDot();
        dots[j][item] = dot;
        if (context.labelsBottom) {
          dot.setBounds (new Rectangle2D.Float (
            domainPos, valueBase - context.graphLengths[j][item],
            context.dotsThickness, context.dotsThickness));
        }
        else {
          dot.setBounds (new Rectangle2D.Float (
            valueBase + context.graphLengths[j][item], domainPos,
            context.dotsThickness, context.dotsThickness));
        }
        area.configureDotShape (
          dot, context.boundsGraph,
          !area.getComponentsColoringByCat() ?
          area.getDotColors()[j] : area.getComponentsColorsByCat()[categoryIndex]);
      }
    }
  }


  private static FancyLine[] buildLines (GraphArea area, LayoutContext context) {

    if ((context.numCats * context.numCompsPerCat) < 2 ||
      context.linesThickness < 1 || context.numLinePoints == 0) {
      return new FancyLine[0];
    }

    FancyLine[] lines = new FancyLine[context.numSets];
    for (int i = 0; i < context.numSets; ++i) {
      lines[i] = new FancyLine();
      area.configureLineShape (
        lines[i], context.linesThickness, area.getLineColors()[i]);
    }

    float catDelta =
      context.domainSize / (float)(context.numLinePoints * context.numCompsPerCat);
    int valueBase = context.labelsBottom ?
      area.getSpaceSizeLocation (Area.MIN).y + area.getSpaceSize (Area.MIN).height :
      area.getSpaceSizeLocation (Area.MIN).x;
    int domainSlotThickness =
      context.allowAlignment ? 0 : context.setsThickness;
    Point[] firstPoints = new Point[context.numSets];

    if (context.numLinePoints == 1) {
      int start = getSingleCategoryStart (
        context, domainSlotThickness, catDelta);
      appendLineCategory (
        area, context, lines, firstPoints, 0, start, catDelta, valueBase);
    }
    else {
      float tickHalf = getTickHalfSize (
        context.categoryTicks[0], context.labelsBottom);
      float minDomain = context.domainStart;
      float maxDomain = getTickPosition (
        context.categoryTicks[0], context.labelsBottom) + tickHalf;
      for (int i = 0; i < context.numLinePoints; ++i) {
        int start = getCategoryStart (
          area, context, minDomain, maxDomain, domainSlotThickness, catDelta);
        appendLineCategory (
          area, context, lines, firstPoints, i, start, catDelta, valueBase);
        if (i + 1 == context.numLinePoints) break;
        minDomain = maxDomain;
        if (i + 2 == context.numLinePoints &&
          area.getLabelsAxisTicksAlignment() == Area.BETWEEN) {
          maxDomain = context.domainStart + context.domainSize;
        }
        else {
          maxDomain = getTickPosition (
            context.categoryTicks[i + 1], context.labelsBottom) + tickHalf;
        }
      }
    }

    for (int i = 0; i < context.numSets; ++i) {
      area.finalizeLineShape (lines[i], context.boundsGraph);
    }
    return lines;
  }


  private static void appendLineCategory (
    GraphArea area, LayoutContext context, FancyLine[] lines, Point[] firstPoints,
    int categoryIndex, int categoryStart, float catDelta, int valueBase) {

    for (int k = 0; k < context.numCompsPerCat; ++k) {
      for (int j = 0; j < context.numSets; ++j) {
        int item = categoryIndex * context.numCompsPerCat + k;
        float domainPos = categoryStart + k * catDelta;
        if (!context.allowAlignment) {
          domainPos += j * context.componentsThickness +
            context.componentsThickness / 2f;
        }

        Point thisPoint = context.labelsBottom ?
          new Point ((int)domainPos, valueBase - context.graphLengths[j][item]) :
          new Point (valueBase + context.graphLengths[j][item], (int)domainPos);

        if (k == 0 && categoryIndex == 0) {
          if (area.getLinesFillInterior()) {
            firstPoints[j] = context.labelsBottom ?
              new Point ((int)domainPos,
                valueBase - area.getLinesFillInteriorBaseValue()) :
              new Point (valueBase + area.getLinesFillInteriorBaseValue(),
                (int)domainPos);
            lines[j].getLine().moveTo (firstPoints[j].x, firstPoints[j].y);
            lines[j].getLine().lineTo (thisPoint.x, thisPoint.y);
          }
          else {
            lines[j].getLine().moveTo (thisPoint.x, thisPoint.y);
          }
        }
        else {
          lines[j].getLine().lineTo (thisPoint.x, thisPoint.y);
          if (area.getLinesFillInterior() &&
            k == context.numCompsPerCat - 1 &&
            categoryIndex == context.numLinePoints - 1) {
            if (context.labelsBottom) {
              lines[j].getLine().lineTo (thisPoint.x, firstPoints[j].y);
            }
            else {
              lines[j].getLine().lineTo (firstPoints[j].x, thisPoint.y);
            }
            lines[j].getLine().lineTo (firstPoints[j].x, firstPoints[j].y);
          }
        }
      }
    }
  }


  private static int getSingleCategoryStart (
    LayoutContext context, int slotThickness, float catDelta) {

    int start = context.domainStart + (context.domainSize - slotThickness) / 2;
    return (int)(start - ((context.numCompsPerCat - 1) / 2f) * catDelta);
  }


  private static int getCategoryStart (
    GraphArea area, LayoutContext context, float minDomain, float maxDomain,
    int slotThickness, float catDelta) {

    int start;
    if (area.getLabelsAxisTicksAlignment() == Area.BETWEEN) {
      start = (int)(minDomain + Math.floor ((maxDomain - minDomain - slotThickness) / 2f));
    }
    else {
      start = (int)Math.floor (maxDomain - slotThickness / 2f);
    }
    return (int)(start - ((context.numCompsPerCat - 1) / 2f) * catDelta);
  }


  private static float getTickHalfSize (
    Rectangle tick, boolean labelsBottom) {

    return labelsBottom ? tick.width / 2f : tick.height / 2f;
  }


  private static float getTickPosition (
    Rectangle tick, boolean labelsBottom) {

    return labelsBottom ? tick.x : tick.y;
  }
}
