package jgrasp_viewers;


import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.AffineTransform;
import java.awt.geom.Line2D;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.Collection;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
import javax.swing.JPanel;
import jgrasp.Colors;
import jgrasp.Drawing;
import jgrasp.KeyMovement;
import jgrasp.PluginOptOut;
import jgrasp.viewer.IndexItem;
import jgrasp.viewer.PaintUtil;
import jgrasp.viewer.Sizes;
import jgrasp.viewer.Util;
import jgrasp.viewer.ViewerCreateData;
import jgrasp.viewer.ViewerException;
import jgrasp.viewer.ViewerInfo;
import jgrasp.viewer.ViewerInitData;
import jgrasp.viewer.ViewerPriorityData;
import jgrasp.viewer.ViewerUpdateData;
import jgrasp.viewer.ViewerValueData;
import jgrasp.viewer.ViewerRoot;
import jgrasp.viewer.gui.ValueDragData;
import jgrasp.viewer.gui.ValueDragger;
import jgrasp.viewer.jgrdi.DebugContext;
import jgrasp.viewer.jgrdi.Method;
import jgrasp.viewer.jgrdi.Type;
import jgrasp.viewer.jgrdi.Value;
import org.w3c.dom.Element;


/** A viewer that displays color bars for arrays and lists of int, Integer, or Color type. **/
public class java__lang__Object_ColorBarView extends ViewerRoot {

   /** State data. **/
   public static class Data {
   
      /** The object value. **/
      private Value obj;
   
      /** The current values. **/
      private List<Integer> values;
   
      /** Changed state for each value. **/
      private List<Boolean> changed;
   
      /** New state for each value. **/
      private List<Boolean> isNew;
   
      /** True if the data was derived from an array type, false if it came from a list. **/
      private boolean isArray;
   
      /** Index expressions. This may be null. **/
      private List<IndexItem> indexes;
   }

   /** Supported element types. **/
   private static final Collection<String> supportedTypes = new HashSet<>();

   static {
      supportedTypes.add("int");
      supportedTypes.add("java.lang.Integer");
      supportedTypes.add("java.awt.Color");
   }

   /** Current state data. **/
   private Data stateData;

   /** Current font height in pixels. **/
   private int fontHeight;

   /** Drawing base unit, in pixels. **/
   private int margin;

   /** Height of index pointers. **/
   private int pointerLen;

   /** Current default size. **/
   private Dimension mainSize;

   /** Rectangle for first bar. **/
   private Rectangle barRect;

   /** Current selected value, or null if there is no selection. **/
   private Value selVal;

   /** Current selected index, or -1 if there is no selection. **/
   private int selInd = -1;
   
   /** Expression for current value, or null if one is not available or there is no selection. **/
   private String selExpr;
   
   /** True if the selection should be scrolled to visible on the next repaint, false otherwise. **/
   private boolean scrollToVis;


   /** Creates a new bar graph viewer.
    *
    * @param vcd viewer creation data. **/
   public java__lang__Object_ColorBarView(final ViewerCreateData vcd) {
      super(false, vcd.supportsSubViewers() ? EnumSet.of(CreationFlags.SUBVIEWER, CreationFlags.FOCUS)
            : EnumSet.of(CreationFlags.FOCUS));
      Value v = vcd.getValue();
      DebugContext dc = vcd.getDebugContext();
      Type t = v.getType(dc);
      try {
         if (!t.isArray(dc)) {
            if (v.isInstanceOf(dc, "java.util.List")) {
               return;
            }
         }
         else {
            Type elementType = t.getArrayElementType(dc);
            String tName = elementType.getName(dc);
            if (supportedTypes.contains(tName) || "java.lang.Object".equals(tName)) {
               return;
            }
         }
      }
      catch (ViewerException ignored) {
      }
      throw new PluginOptOut();
   }


   /** {@inheritDoc} **/
   @Override
   public void build(final ViewerInitData vid, final Element initDataIn) {
      super.build(vid, initDataIn);
      vid.setIndexable(true);
   }


   /** {@inheritDoc} **/
   @Override
   public void configure() {
      Data currentData = stateData;
      if (currentData == null) {
         return;
      }
   
      JPanel mp = getMainPanel();
      Graphics2D g = (Graphics2D) mp.getGraphics();
      if (g == null) {
         return;
      }
      setBaseTransformAndAdjust(g);
      g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
   
      g.setFont(mp.getFont());
      FontMetrics fm = g.getFontMetrics();
      fontHeight = fm.getHeight();
      margin = (fm.getHeight() + 3) / 4;
      List<Integer> currentValues = currentData.values;
      if (currentValues == null) {
         return;
      }
      int w = Math.max(50, currentValues.size()) * margin;
      int labelHeight = margin * 2 + fm.getHeight();
      int h = margin * 10 + labelHeight;
      if (currentData.indexes != null) {
         // Marker space.
         h += pointerLen * 2 + margin * 2;
         // Label space.
         h += fontHeight * currentData.indexes.size();
      }
   
      Dimension size = new Dimension(w, h);
      boolean sizeChanged = false;
      if (!size.equals(mainSize)) {
         mainSize = size;
         sizeChanged = true;
      }
      int minHeight = margin * 5 + labelHeight;
   
      pointerLen = (fontHeight + 2) / 4;
   
      if (currentData.indexes != null) {
         // Marker space.
         minHeight += pointerLen * 2 + margin * 2;
         // Label space.
         minHeight += fontHeight * currentData.indexes.size();
      }
   
      int count = currentValues.size();
      Dimension minSize = new Dimension(count + 2 * margin, minHeight);
   
      drawingToJava(minSize);
      if (!mp.getPreferredSize().equals(minSize)) {
         mp.setPreferredSize(minSize);
         sizeChanged = true;
      }
      if (sizeChanged) {
         mp.revalidate();
         mp.validate();
      }
      mp.repaint();
   }


   /** {@inheritDoc} **/
   @Override
   public void getInfo(final ViewerInfo vi) {
      vi.setShortDescription("Viewer for int, Integer, and java.awt.Color arrays and lists");
      vi.setLongDescription("This viewer displays arrays and java.util.List as bars of color using the color of each "
            + "value if the value is a java.awt.Color, or it's corresponding ARGB color if it's an int or integer.");
   }


   /** {@inheritDoc} **/
   @Override
   public Dimension getMainSize() {
      return mainSize;
   }


   /** {@inheritDoc} **/
   @Override
   public String getMainToolTipText(final MouseEvent e) {
      Data currentData = stateData;
      if (currentData == null || barRect == null) {
         return null;
      }
      Point pt = e.getPoint();
      javaToDrawing(pt);
      if (pt.y < barRect.y || pt.y > barRect.y + barRect.height) {
         return null;
      }
      int ind = (pt.x - barRect.x) / barRect.width;
      if (ind < 0) {
         return null;
      }
      if (ind >= currentData.values.size()) {
         return null;
      }
      int argb = currentData.values.get(ind).intValue();
      String val = (((argb & 0xff000000) != 0xff000000) ? "Alpha = " + (argb >> 24) + ", " : "") + "R = "
            + ((argb & 0xff0000) >> 16) + ", G = " + ((argb & 0xff00) >> 8) + ", B = " + (argb & 0xff);
      if (currentData.isArray) {
         return "[" + ind + "] = " + val;
      }
      return "<" + ind + "> = " + val;
   }


   /** {@inheritDoc} **/
   @Override
   public int getPriority(final ViewerPriorityData vpd) {
      Value v = vpd.getValue();
      DebugContext context = vpd.getDebugContext();
      try {
         if (v == null) {
            return Integer.MIN_VALUE;
         }
         Type type = v.getType(context);
         if (!type.isArray(context)) {
            if (v.isInstanceOf(context, "java.util.List")) {
               return -10;
            }
            return Integer.MIN_VALUE;
         }
         Type elementType = type.getArrayElementType(context);
         String elementTypeName = elementType.getName(context);
         if (!supportedTypes.contains(elementTypeName) && !"java.lang.Object".equals(elementTypeName)) {
            return Integer.MIN_VALUE;
         }
      }
      catch (ViewerException e) {
         return Integer.MIN_VALUE;
      }
      return -10;
   }


   /** {@inheritDoc} **/
   @Override
   public String getViewName() {
      return "Color Bars";
   }


   /** {@inheritDoc} **/
   @Override
   public void paintMainView(final Graphics2D graphics, final JPanel paintPanel) {
      AffineTransform prevTransform = graphics.getTransform();
      setBaseTransform(graphics);
      
      int lw = Sizes.getVeryThinEdge(fontHeight);
      float lOffs = (lw % 2) / 2.0f;
      if (!isViewerTransparent()) {
         graphics.setColor(Drawing.getColor(Colors.OUTLINE));
         int ppw = javaToDrawingW(paintPanel.getWidth());
         int pph = javaToDrawingH(paintPanel.getHeight());
         graphics.fillRect(0, 0, ppw, pph);
         if (paintPanel.getWidth() > lw + lw) {
            graphics.setColor(Drawing.getColor(Colors.BG));
            graphics.fillRect(lw, lw, ppw - lw - lw, pph - lw - lw);
         }
      }
      Data currentData = stateData;
      List<Integer> currentValues = currentData.values;
      if (currentValues == null) {
         return;
      }
   
      graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
   
      Dimension viewSize = paintPanel.getSize();
      javaToDrawing(viewSize);
      graphics.setFont(scaleFont(paintPanel.getFont()));
      FontMetrics fm = graphics.getFontMetrics();
      int fontAscent = fm.getAscent();
   
      int ht = viewSize.height - margin * 4 - fontAscent - fontAscent / 2;
      if (currentData.indexes != null) {
         // Marker space.
         ht -= (graphics.getFontMetrics().getHeight() + 2) / 4;
         ht -= margin * 2;
         // Label space.
         ht -= graphics.getFontMetrics().getHeight() * currentData.indexes.size();
      }
      if (ht < margin) {
         ht = margin;
      }
   
      List<Boolean> changed = currentData.changed;
      List<Integer> values = currentData.values;
   
      int zeroLine;
      int numberLine;
      zeroLine = margin + ht + fontAscent / 2;
      numberLine = zeroLine;
   
      int w = viewSize.width - margin - margin;
      int size = currentValues.size();
      if (w < size) {
         w = size;
      }
   
      int maxLabelLen = (int) Math.log10(Math.max(1, size - 1)) + 1;
      int maxLabelW = (int) Math.ceil(
            fm.getStringBounds("000000000000000000", 0, Math.min(18, maxLabelLen), graphics).getWidth());
      int spaceSize = size + ((currentData.indexes != null)? 1 : 0);
      int barW = spaceSize > 0 ? w / spaceSize : 0;
   
      int minLabelSep = (int) Math.ceil((maxLabelW + margin) / (double) barW);
      if (minLabelSep < 1) {
         minLabelSep = 1;
      }
      // Label on the 5s or the 10s.
      int label10Sep = (int) Math.ceil(Math.pow(10, Math.ceil(Math.log10(minLabelSep))));
      int label5Sep = 5 * (int) Math.ceil(Math.pow(10, Math.ceil(Math.log10(minLabelSep) - Math.log10(5))));
      int labelSep = Math.min(label10Sep, label5Sep);
   
      int halfMargin = margin / 2;
      int x = margin;
      graphics.setColor(Drawing.getColor(Colors.FG));
      graphics.setStroke(new BasicStroke(lw, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_MITER, 2.0f));
   
      barRect = new Rectangle(x, zeroLine -ht, barW, ht);
   
      startBarPaint();
      Rectangle2D.Double selRect = null;
      String accText = null;
      for (int v = 0; v < size; v++, x += barW) {
         int barH;
         int val = values.get(v).intValue();
         boolean change = changed != null && changed.get(v).booleanValue();
         boolean isNew = currentData.isNew != null && currentData.isNew.get(v).booleanValue();
         barH = change ? (ht * 2 + 1) / 3 : (isNew ? (ht + 1) / 3 : ht);
         int y = zeroLine - barH;
         if (change) {
            changedValue(v, size, val);
         }
         graphics.setColor(new Color(currentValues.get(v).intValue()));
         Rectangle2D.Double rect = new Rectangle2D.Double(x + (barW == 1 ? 1 : 0) + lOffs, y + lOffs, barW, barH);
         if (v == selInd) {
            selRect = new Rectangle2D.Double(rect.x, barRect.y, barW, barRect.height);
            int argb = currentValues.get(v).intValue();
            String txt = (((argb & 0xff000000) != 0xff000000) ? "Alpha = " + (argb >> 24) + ", " : "") + "R = "
               + ((argb & 0xff0000) >> 16) + ", G = " + ((argb & 0xff00) >> 8) + ", B = " + (argb & 0xff);
            accText = "Bar " + v + "\n" + txt;
         }
         graphics.fill(rect);
         if (barW > 1) {
            graphics.setColor(Drawing.getColor(Colors.OUTLINE));
            rect.height = lw;
            graphics.draw(rect);
         }
      
         if (v % labelSep == 0) {
            String label = String.valueOf(v);
            int labelWidth = (int) Math.ceil(fm.getStringBounds(label, graphics).getWidth());
            int linePos = x + barW / 2;
            graphics.setColor(Drawing.getColor(Colors.FG));
            y = numberLine + fontAscent + margin * 2;
            graphics.draw(new Line2D.Float(linePos + lOffs, numberLine + halfMargin + lOffs, linePos + lOffs,
                  numberLine + margin + halfMargin + lOffs));
            graphics.drawString(label, (int) (x + barW / 2.0 - labelWidth / 2.0), y);
         }
      }
      Drawing.drawSelectionRect(graphics, selRect, lw);
      endBarPaint();
      graphics.setColor(Drawing.getColor(Colors.FG));
      graphics.draw(new Line2D.Float(margin + lOffs, zeroLine + lOffs,
            margin + barW * size + lOffs, zeroLine + lOffs));
      graphics.draw(new Line2D.Float(margin + lOffs, zeroLine - ht + lOffs, margin + lOffs, zeroLine + lOffs));
   
      if (currentData.indexes != null) {
         int[] xCenters = new int[size + 1];
         for (int i = 0; i < size + 1; i++) {
            xCenters[i] = margin + i * barW + barW / 2;
         }
         PaintUtil.paintHorizontalIndexes(graphics, xCenters, numberLine + fontAscent + margin * 2 + margin, margin,
               viewSize.width - margin, pointerLen, currentData.indexes, 0);
      }
      graphics.setTransform(prevTransform);
      
      if (selRect != null && scrollToVis) {
         Rectangle rect = selRect.getBounds();
         drawingToJava(rect);
         paintPanel.scrollRectToVisible(rect);
      }
      scrollToVis = false;
      if (accText == null) {
         accText = "Element count " + size;
      }
      paintPanel.getAccessibleContext().setAccessibleDescription(accText);
   }


   /** {@inheritDoc} **/
   @Override
   public void updateState(final ViewerValueData valueData, final ViewerUpdateData data, final DebugContext context)
         throws ViewerException {
      selExpr = getExpr(valueData, selInd, context);
      Data oldData = stateData;
      Value value = valueData.value();
      Type type = value.getType(context);
      Data newData = new Data();
      newData.obj = value;
      if (!type.isArray(context)) {
         if (value.isInstanceOf(context, "java.util.List")) {
            Method toArrayMethod = type.getMethod(context, "toArray", "java.lang.Object[]", null);
            value = value.invokeMethod(context, toArrayMethod, null);
            type = value.getType(context);
         }
         else {
            stateData = newData;
            return;
         }
      }
      else {
         newData.isArray = true;
      }
      Type elementType = type.getArrayElementType(context);
      boolean isObject = elementType.isObject(context);
   
      int len = value.getArrayLength(context);
      List<Integer> newValues = null;
      Method intValue = null;
      Type lastType = null;
      List<Value> values = value.getArrayElements(context, 0, len);
      selVal = null;
      for (int valInd = 0; valInd < len; valInd++) {
         Value v = values.get(valInd);
         if (valInd == selInd) {
            selVal = v;
         }
         Type t = isObject? v.getType(context) : elementType;
         String tName = t.getName(context);
         if (newValues == null) {
            newValues = new ArrayList<>(len);
         }
      
         if (!supportedTypes.contains(tName)) {
            newValues.add(Integer.valueOf(0));
            continue;
         }
      
         if (isObject) {
            if (v.isNull(context)) {
               newValues.add(Integer.valueOf(0));
            }
            else {
               if (!t.equals(lastType)) {
                  if (tName.equals("java.awt.Color")) {
                     intValue = t.getMethod(context, "getRGB", "int", null);
                  }
                  else {
                     intValue = t.getMethod(context, "intValue", "int", null);
                  }
               }
               lastType = t;
               newValues.add(Integer.valueOf(v.invokeMethod(context, intValue, null).toInt(context)));
            }
         }
         else {
            newValues.add(Integer.valueOf(v.toInt(context)));
         }
      }
      newData.values = newValues;
      if (oldData != null && oldData.values != null && newValues != null) {
         boolean valueChanged = !oldData.obj.equals(newData.obj);
         newData.changed = new ArrayList<>(newValues.size());
         newData.isNew = new ArrayList<>(newValues.size());
         for (int i = 0; i < newValues.size(); i++) {
            boolean changed = !valueChanged && (i >= oldData.values.size()
                  || !oldData.values.get(i).equals(newValues.get(i)));
            newData.changed.add(Boolean.valueOf(changed));
            boolean isNew = valueChanged || i >= oldData.values.size();
            newData.isNew.add(Boolean.valueOf(isNew));
         }
      }
   
      newData.indexes = IndexItem.evalIndexExpressions(valueData.indexExpressions(),
            oldData == null ? null : oldData.indexes, context);
   
      stateData = newData;
   
      setSubViewValue(context, selVal, data.isReset());
   }
   
   
   /** Called before the painting of the bars starts. The default implementation does nothing. **/
   public void startBarPaint() {
   }
   
   
   /** Called during the painting of the bars, when a changed value is encountered. The default implementation does
    * nothing.
    *
    * @param index the index of the changed bar.
    *
    * @param size the number of elements in the graph.
    *
    * @param val the RGB value of the number. **/
   public void changedValue(final int index, final int size, final int val) {
   }
   
   
   /** Called after the painting of the bars starts. The default implementation does nothing. **/
   public void endBarPaint() {
   }
   
   
   /** {@inheritDoc} **/
   @Override
   public void buildGui(final JPanel main) {
      super.buildGui(main);
      main.addMouseListener(
         new MouseAdapter() {
         
            @Override
            public void mousePressed(final MouseEvent e) {
               if (barRect == null) {
                  return;
               }
            
               selInd = -1;
               selExpr = null;
            
               int ind;
               Point pt = e.getPoint();
               javaToDrawing(pt);
               if (pt.y < barRect.y || pt.y > barRect.y + barRect.height) {
                  ind = -1;
               }
               else {
                  ind = (pt.x - barRect.x) / barRect.width;
               }
               if (ind == selInd) {
                  return;
               }
            
               selInd = ind;
               selChanged();
            }
         });
      ValueDragger.createValueDragger(main, getVIData(), 
         (pos)-> {
            if (selVal == null) {
               return null;
            }
            return new ValueDragData(selExpr, selVal, null, getVIData().getScope(), getClass().getName());
         });
   
      main.getAccessibleContext().setAccessibleName(getViewName() + " viewer for " + getVIData().getName());
      Util.setupCopyForAccessibleDescription(main);
      KeyMovement.setup(main, (dir, block, expand) -> keyMove(dir, block, expand));
   }
   
   
   /** Gets the expression for a value at a specified index.
    *
    * @param valueData the value update data.
    *
    * @param index the index of interest.
    *
    * @return an expression for the value at the requested index, or null if there is no expression for the current
    * value. **/
   public String getExpr(final ViewerValueData valueData, final int index, final DebugContext context) {
      if (index < 0) {
         return null;
      }
      String expr = getVIData().getExpression();
      if (expr == null) {
         return null;
      }
      
      String end;
      boolean needCast;
      Value val = valueData.value();
      Type dt = valueData.declaredType();
      if (val.isArray(context)) {
         end = "[" + index + "]";
         needCast = !dt.isArray(context);
      }
      else if (val.isInstanceOf(context, "java.util.List")) {
         end = ".get(" + index + ")";
         needCast = !dt.isInstanceOf(context, "java.util.List");
      }
      else {
         return null;
      }
      if (!needCast) {
         if (getVIData().getNeedParen()) {
            return "(" + expr + ")" + end;
         }
         return expr + end;
      }
      Value value = valueData.value();
      String cast = value.getType(context).getName(context);
      return "((" + (cast.indexOf('.') >= 0 ? "`cast`" : "") + cast + ") " + expr + ")" + end;
   }


   /** Selects, scrolls, or changes the selection.
    *
    * @param dir the action and direction of change, 0 for select/unselect, -1 for left, 1 for right, -2 for up, 2 for
    * down.
    *
    * @param block true if the amount of change should be a block, false if it should be one unit.
    *
    * @param expand true if the selection should be expanded, false if it should not. **/
   private void keyMove(final int dir, final boolean block, final boolean expand) {
      Data data = stateData;
      if (data == null || data.values.isEmpty()) {
         return;
      }
      if (dir == 0) {
         if (expand == (selInd < 0)) {
            return;
         }
         selInd = (selInd < 0) ? 0 : -1;
         selChanged();
         return;
      }
      if (selInd < 0) {
         Util.scrollPaneScroll(getMainPanel(), dir, block);
         return;
      }
      selInd = (selInd + data.values.size() + ((dir < 0)? -1 : 1)) % data.values.size();
      selChanged();
   }


   /** Called when the selection changes. **/
   private void selChanged() {
      getVIData().update();
      if (selInd >= 0) {
         scrollToVis = true;
      }
   }
}
