package jgrasp_viewers;


import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.util.Collections;
import java.util.List;
import javax.swing.JComboBox;
import javax.swing.JLabel;
import javax.swing.JPanel;
import jgrasp.PluginOptOut;
import jgrasp.UndoComboBox;
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.XMLUtil;
import jgrasp.viewer.jgrdi.DebugContext;
import jgrasp.viewer.jgrdi.Field;
import jgrasp.viewer.jgrdi.Method;
import jgrasp.viewer.jgrdi.Type;
import jgrasp.viewer.jgrdi.Value;
import org.w3c.dom.Document;
import org.w3c.dom.Element;


/** An Object viewer that displays the elements as a 2D image. **/
public class _X_ImageView extends ImageView {
   //*** Need to make this scroll when the max size is exceeded.


   /** Color mapping modes. **/
   protected enum ColorMode {
   
      /** RGB mode (highest byte unused). **/
      RGB("RGB") {
         @Override
         public int toARGB(final int v) {
            return 0xff000000 | v;
         }
      },
   
      /** ARGB mode. **/
      ARGB("ARGB") {
         @Override
         public int toARGB(final int v) {
            return v;
         }
      },
      
      /** BGR mode (highest byte unused). **/
      BGR("BGR") {
         @Override
         public int toARGB(final int v) {
            return 0xff000000 | ((v >> 16) & 0xff) | ((v & 0xff) << 16) | (v & 0xff00);
         }
      },
   
      /** ABGR mode. **/
      ABGR("ABGR") {
         @Override
         public int toARGB(final int v) {
            return ((v >> 16) & 0xff) | ((v & 0xff) << 16) | (v & 0xff00ff00);
         }
      };
      
      
      /** The mode display label. **/
      private final String label;
      
      
      /** Creates a new Mode.
       *
       * @param labelIn the display label for the Mode. **/
      ColorMode(final String labelIn) {
         label = labelIn;
      }
      
      
      /** {@inheritDoc} **/
      @Override
      public String toString() {
         return label;
      }
      
      
      /** Maps an int value to an ARGB int value.
       *
       * @param v the int value.
       *
       * @return ARGB value mapped by this mode when applied to <code>v</code>. **/
      public abstract int toARGB(int v);
   }


   /** Value getter methods and fields. **/
   private static class MethodsAndFields {
   
      /** Integer.intValue method. **/
      public Method intValue;
      
      /** The value field for a java.lang.Integer. **/
      public Field valueField;
      
      /** The value field for a java.awt.Color. **/
      public Field colorValueField;
   }


   //** Maximum width and height. **/
   private static final int MAX_SIZE = 1000;

   /** The default initial color mapping mode. **/
   private static final ColorMode initColorMode = ColorMode.RGB;

   /** The color mapping mode. **/
   private ColorMode colorMode = initColorMode;


   /** Creates a new image viewer for 2D arrays or lists.
    *
    * @param vcd viewer creation data. **/
   public _X_ImageView(final ViewerCreateData vcd) {
      super(vcd);
      Value v = vcd.getValue();
      DebugContext dc = vcd.getDebugContext();
      if (v.isNull(dc)) {
         throw new PluginOptOut();
      }
      try {
         if (v.getType(dc).getName(dc).endsWith("[][]") || (ArrayOrListUtil.getLength(v, dc) > 0
               && ArrayOrListUtil.isArrayOrList(ArrayOrListUtil.getValue(v, 0, dc), dc))) {
            return;
         }
      }
      catch (ViewerException ignored) {
      }
      throw new PluginOptOut();
   }


   /** {@inheritDoc} **/
   @Override
   public void getInfo(final ViewerInfo vi) {
      vi.setShortDescription("2D image viewer");
      vi.setLongDescription("This viewer displays the value for 2D array and list elements as an image. Integer "
         + "values will be interpreted as RGB, other numbers as zero = black and non-zero = white, java.awt.Color as "
         + "a color, and other objects as null = black and non-null = white.");
   }


   /** {@inheritDoc} **/
   @Override
   public int getPriority(final ViewerPriorityData vpd) {
      return -1;
   }


   /** {@inheritDoc} **/
   @Override
   public String getViewName() {
      return "2D Image";
   }


   /** {@inheritDoc} **/
   @Override
   public Data getData(final ViewerValueData valueData, final ViewerUpdateData data, final DebugContext context)
         throws ViewerException {
      Value value = valueData.value();
      int rows = Math.min(ArrayOrListUtil.getLength(value, context), MAX_SIZE);
   
      int longestCol = 0;
      List<Value> rowValues = ArrayOrListUtil.getValues(value, 0, rows, context);
      if (rowValues == null) {
         rowValues = Collections.emptyList();
      }
   
      for (Value row : rowValues) {
         int cols = (row == null || row.isNull(context))? 0 : ArrayOrListUtil.getLength(row, context);
         if (cols > longestCol) {
            longestCol = cols;
         }
      }
      
      int[] pixels = new int[rows * longestCol];
      int pixelInd = 0;
   
      MethodsAndFields maf = new MethodsAndFields();
      for (Value row : rowValues) {
         int cols = (row == null || row.isNull(context))? 0
               : Math.min(ArrayOrListUtil.getLength(row, context), MAX_SIZE);
         if (cols > 0) {
            List<Value> values = ArrayOrListUtil.getValues(row, 0, cols, context);
            if (values == null) {
               values = Collections.emptyList();
            }
         
            if (row.isArray(context)) {
               Type t = row.getType(context).getArrayElementType(context);
               String tName = t.getName(context);
               if (tName.equals("int")) {
                  for (int j = 0; j < cols; j++) {
                     pixels[pixelInd++] = values.get(j).toInt(context);
                  }
               }
               else if (tName.equals("java.lang.Integer")) {
                  if (maf.valueField == null) {
                     maf.valueField = t.getField("value");
                  }
                  for (int j = 0; j < cols; j++) {
                     pixels[pixelInd++] = values.get(j).getFieldValue(context, maf.valueField).toInt(context);
                  }
               }
               else {
                  for (int j = 0; j < cols; j++) {
                     pixels[pixelInd++] = getInt(values.get(j), maf, context);
                  }
               }
            }
            else {
               for (int j = 0; j < cols; j++) {
                  pixels[pixelInd++] = getInt(values.get(j), maf, context);
               }
            }
         }
         // Leave zeros at end of shorter columns.
         pixelInd += longestCol - cols;
      }
      
      return new Data(longestCol + " x " + rows, null, pixels, longestCol, rows, true);
   }
   
   
   /** Gets the packed color component values for a value.
    *
    * @param v the pixel value.
    *
    * @param maf cached methods and fields for pixel value computations.
    *
    * @param context the current debug context.
    *
    * @return the integer color components for the value, packed into an int. **/
   private static int getInt(final Value v, final MethodsAndFields maf, final DebugContext context) {
      Type t = v.getType(context);
      String tName = t.getName(context);
      boolean on = false;
      try {
         switch (tName) {
            case "int" :
               return v.toInt(context);
            case "long" :
               on = v.toLong(context) != 0L;
               break;
            case "short" :
               on = (int) v.toShort(context) != 0;
               break;
            case "char" :
               on = (int) v.toChar(context) != 0;
               break;
            case "byte" :
               on = (int) v.toByte(context) != 0;
               break;
            case "float" :
               on = v.toFloat(context) != 0.0f;
               break;
            case "double" :
               on = v.toDouble(context) != 0.0;
               break;
            case "boolean" :
               on = v.toBoolean(context);
               break;
            case "java.lang.Integer":
               if (maf.valueField == null) {
                  maf.valueField = t.getField("value");
               }
               return v.getFieldValue(context, maf.valueField).toInt(context);
            case "java.awt.Color":
               if (maf.colorValueField == null) {
                  maf.colorValueField = t.getField("value");
               }
               return v.getFieldValue(context, maf.colorValueField).toInt(context);
            default:
               on = !v.isNull(context);
         }
      }
      catch (ViewerException ignored) {
      }
      return on? 0xffffff : 0;
   }


   /** {@inheritDoc} **/
   @Override
   public void addSettingsItems(final JPanel panel, final GridBagLayout gbl, final GridBagConstraints constraints) {
      GridBagConstraints gbc = (GridBagConstraints) constraints.clone();
   
      JLabel label = new JLabel("Color Mode");
      gbc.insets.bottom = 0;
      gbc.gridwidth = 1;
      gbc.fill = GridBagConstraints.NONE;
      gbc.anchor = GridBagConstraints.EAST;
      gbc.weightx = 0.001;
      panel.add(label);
      gbl.setConstraints(label, gbc);
      
      JComboBox<ColorMode> modeChoice = new UndoComboBox<>("Color Mode");
      for (ColorMode mode : ColorMode.class.getEnumConstants()) {
         modeChoice.addItem(mode);
      }
      modeChoice.setSelectedItem(colorMode);
      modeChoice.addActionListener(
         (e)->{
            if (!e.getActionCommand().equals("comboBoxChanged")) {
               return;
            }
            colorMode = modeChoice.getItemAt(modeChoice.getSelectedIndex());
            updateGui();
            getVIData().stateChanged();
         });
      gbc.gridwidth = GridBagConstraints.REMAINDER;
      gbc.fill = GridBagConstraints.HORIZONTAL;
      gbc.anchor = GridBagConstraints.WEST;
      gbc.weightx = 1.0;
      panel.add(modeChoice);
      gbl.setConstraints(modeChoice, gbc);
   }


   /** {@inheritDoc} **/
   @Override
   public void build(final ViewerInitData vid, final Element initDataIn) {
      super.build(vid, initDataIn);
      if (initDataIn == null) {
         return;
      }
      colorMode = XMLUtil.getEnum(initDataIn, "ColorMode", ColorMode.class, colorMode);
   }


   /** {@inheritDoc} **/
   @Override
   public boolean toXML(final Document doc, final Element e) {
      if (colorMode == initColorMode) {
         return false;
      }
      XMLUtil.addXMLString(doc, e, "ColorMode", colorMode.name());
      return true;
   }
   
   
   /** {@inheritDoc} **/
   @Override
   public int[] convertPixels(final int[] pixels) {
      if (colorMode == ColorMode.ARGB) {
         return pixels;
      }
      int[] pixelsOut = new int[pixels.length];
      for (int i = 0; i < pixels.length; i++) {
         pixelsOut[i] = colorMode.toARGB(pixels[i]);
      }
      return pixelsOut;
   }

}
