package jgrasp_viewers;


import java.awt.BorderLayout;
import java.awt.Font;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.util.EnumSet;
import javax.swing.JComboBox;
import javax.swing.JLabel;
import javax.swing.JPanel;
import jgrasp.SharedPluginUtil;
import jgrasp.UndoComboBox;
import jgrasp.viewer.PaintUtil;
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.ViewerRoot;
import jgrasp.viewer.ViewerValueData;
import jgrasp.viewer.ViewerUpdateData;
import jgrasp.viewer.XMLUtil;
import jgrasp.viewer.jgrdi.DebugContext;
import org.w3c.dom.Document;
import org.w3c.dom.Element;


/** A base class for image viewers. **/
public abstract class ImageView extends ViewerRoot {


   /** View modes. **/
   protected enum Mode {
   
      NORMAL("Normal"),
   
      TRANSPARENCY("Show Transparency as Shades of Gray");
      
      
      /** The mode display label. **/
      private final String label;
      
      
      /** Creates a new Mode.
       *
       * @param labelIn the display label for the Mode. **/
      Mode(final String labelIn) {
         label = labelIn;
      }
      
      
      /** {@inheritDoc} **/
      @Override
      public String toString() {
         return label;
      }
   }


   /** Data for the current image. **/
   public static class Data {
   
      /** The display size description. **/
      private final String dsText;
   
      /** Text supplied by the subclass. **/
      private final String clientText;
   
      /** The pixel data, or null if the image is not available. **/
      private final int[] pixelData;
   
      /** The width of the image. **/
      private final int w;
   
      /** The height of the image. **/
      private final int h;
      
      /** True if the image data is complete, false otherwise. **/
      private final boolean complete;
   
   
      /** Creates a new Data.
       *
       * @param dsTextIn the display size description.
       *
       * @param clientTextIn text supplied by the subclass.
       *
       * @param pixelDataIn the pixel data, or null if the image is not available.
       *
       * @param wIn the width of the image supplied in <code>pixelData</code>.
       *
       * @param hIn the height of the image supplied in <code>pixelData</code>.
       *
       * @param completeIn true if the image data is complete, false otherwise. **/
      Data(final String dsTextIn, final String clientTextIn, final int[] pixelDataIn, final int wIn, final int hIn,
            final boolean completeIn) {
         dsText = dsTextIn;
         clientText = clientTextIn;
         pixelData = pixelDataIn;
         w = wIn;
         h = hIn;
         complete = completeIn;
      }
   }

   /** Maximum preferred image display width. **/
   private static final int MAX_WIDTH = 2000;

   /** Maximum preferred image display height. **/
   private static final int MAX_HEIGHT = 2000;
   
   /** Transparency to color mapping for transparency view mode. **/
   private static final int[] alphaToARGB = new int[256];
   
   static {
      for (int i = 0; i < 256; i++) {
         alphaToARGB[i] = 0xff000000 + (i << 16) + (i << 8) + i;
      }
   }

   /** The current image data. **/
   private Data currentData;

   /** The image properties display label. **/
   private JLabel displayLabel;

   /** The gui root panel. **/
   private JPanel imagePanel;

   /** The display image. **/
   private BufferedImage displayImage;

   /** Display text describing image size. **/
   private String displaySizeText;

   /** Last display text provided by subclass. **/
   private String currentClientText;

   /** True if the image was complete when captured, false otherwise. **/
   private boolean isComplete;

   /** The default initial mode. **/
   private static final Mode initMode = Mode.NORMAL;

   /** The display mode. **/
   private Mode mode = initMode;
   

   /** Creates a new image viewer.
    *
    * @param vcd viewer creation data. **/
   public ImageView(final ViewerCreateData vcd) {
      super(false, EnumSet.of(CreationFlags.STANDARD_BORDER));
   }


   /** {@inheritDoc} **/
   @Override
   public void buildGui(final JPanel main) {
      buildImagePanel();
      main.setLayout(new BorderLayout());
      main.add(imagePanel, "Center");
      buildControls();
   }


   /** {@inheritDoc} **/
   @Override
   public void getInfo(final ViewerInfo vi) {
      vi.setShortDescription("Detail viewer for images");
      vi.setLongDescription("This viewer displays the image at actual size up to a maximum of 250x250 pixels.");
   }


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


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


   /** {@inheritDoc} **/
   @Override
   public void updateGui() {
      Data data = currentData;
   
      displaySizeText = data.dsText;
      currentClientText = data.clientText;
   
      if (data.pixelData == null) {
         displayImage = null;
         imagePanel.getAccessibleContext().setAccessibleDescription("null image");
      }
      else {
         //noinspection StringConcatenationMissingWhitespace
         imagePanel.getAccessibleContext().setAccessibleDescription("image size: " + data.w + "x" + data.h);
         displayImage = new BufferedImage(data.w, data.h, BufferedImage.TYPE_INT_ARGB);
         int[] pixels = convertPixels(data.pixelData);
         if (mode == Mode.TRANSPARENCY) {
            //noinspection ArrayEquality
            if (pixels == data.pixelData) {
               // Need a new array only if convertPixels() did not already create one.
               pixels = new int[data.w * data.h];
            }
            for (int p = 0; p < pixels.length; p++) {
               int alpha = (data.pixelData[p] >> 24) & 0xff;
               pixels[p] = alphaToARGB[alpha];
            }
         }
         displayImage.setRGB(0, 0, data.w, data.h, pixels, 0, data.w);
         imagePanel.revalidate();
         getMainPanel().revalidate();
      }
      isComplete = data.complete;
   
      imagePanel.repaint();
   }


   /** Builds the controls. **/
   private void buildControls() {
      JPanel panel = new JPanel();
      displayLabel = new JLabel(" ");
      GridBagLayout gbl = new GridBagLayout();
      GridBagConstraints constraints = new GridBagConstraints();
      Insets insets = constraints.insets;
      int spacing = 8;
      Font font = panel.getFont();
      if (font != null) {
         spacing = panel.getFontMetrics(font).getHeight() / 2;
      }
      panel.setLayout(gbl);
   
      constraints.weighty = 0.001;
      insets.top = spacing;
      insets.left = spacing;
      insets.right = spacing;
      insets.bottom = spacing;
   
      addSettingsItems(panel, gbl, constraints);
   
      JLabel label = new JLabel("Mode");
      constraints.gridwidth = 1;
      constraints.fill = GridBagConstraints.NONE;
      constraints.anchor = GridBagConstraints.EAST;
      constraints.weightx = 0.001;
      panel.add(label);
      gbl.setConstraints(label, constraints);
      
      JComboBox<Mode> modeChoice = new UndoComboBox<>("Image Mode");
      for (Mode md : Mode.class.getEnumConstants()) {
         modeChoice.addItem(md);
      }
      modeChoice.setSelectedItem(mode);
      modeChoice.addActionListener(
         (e)->{
            if (!e.getActionCommand().equals("comboBoxChanged")) {
               return;
            }
            mode = modeChoice.getItemAt(modeChoice.getSelectedIndex());
            updateGui();
            getVIData().stateChanged();
         });
      
      constraints.gridwidth = GridBagConstraints.REMAINDER;
      constraints.fill = GridBagConstraints.HORIZONTAL;
      constraints.anchor = GridBagConstraints.WEST;
      constraints.weightx = 1.0;
      panel.add(modeChoice);
      gbl.setConstraints(modeChoice, constraints);
         
      insets.top = 0;
      displayLabel = new JLabel(" ");
      panel.add(displayLabel);
      gbl.setConstraints(displayLabel, constraints);
   
      getVIData().setControls(panel);
   }


   /** Builds the main display panel. **/
   private void buildImagePanel() {
      imagePanel = 
         new JPanel() {
         
            @Override
            public Dimension getPreferredSize() {
               Insets insets = getInsets();
               if (displayImage == null) {
                  return new Dimension(10 + insets.left + insets.right, 10 + insets.top + insets.bottom);
               }
               double pixelScale = SharedPluginUtil.getPixelScale(getGraphicsConfiguration()).y;
               int imageWidth = displayImage.getWidth();
               int imageHeight = displayImage.getHeight();
               return new Dimension(Math.min((int) Math.ceil(imageWidth / pixelScale) + insets.left + insets.right,
                     MAX_WIDTH), Math.min((int) Math.ceil(imageHeight / pixelScale) + insets.top + insets.bottom,
                     MAX_HEIGHT));
            }
          
          
            @Override
            public Dimension getMinimumSize() {
               return getPreferredSize();
            }
            
            
            /** {@inheritDoc} **/
            @Override
            public boolean isOpaque() {
               return !isViewerTransparent();
            }
            
            
            @Override
            public void paintComponent(final Graphics g) {
               super.paintComponent(g);
               int width = getWidth();
               int height = getHeight();
               if (!isViewerTransparent()) {
                  g.setColor(getBackground());
                  g.fillRect(0, 0, width, height);
               }
               if (displayImage == null) {
                  return;
               }
               Insets insets = getInsets();
               int borderXOffs = insets.left;
               int borderYOffs = insets.top;
               width -= insets.left + insets.right;
               height -= insets.top + insets.bottom;
               if (width < 0) {
                  width = 0;
               }
               if (height < 0) {
                  height = 0;
               }
               int imageWidth = displayImage.getWidth();
               int imageHeight = displayImage.getHeight();
               AffineTransform xform = ((Graphics2D) g).getTransform();
               double sx = xform.getScaleX();
               double sy = xform.getScaleY();
               int scaledWidth = (int) (width * sx);
               int scaledHeight = (int) (height * sx);
               Image drawImage;
               boolean scaled = false;
               if (imageWidth == scaledWidth && imageHeight <= scaledHeight
                     || imageHeight == scaledHeight && imageWidth <= scaledWidth) {
                  drawImage = displayImage;
               }
               else {
                  double scale = Math.min((double) scaledWidth / imageWidth, (double) scaledHeight / imageHeight);
                  if (scale > 1.0) {
                     scale = Math.floor(scale);
                  }
                  if (scale == 1.0) {
                     drawImage = displayImage;
                  }
                  else {
                     scaled = true;
                     int intScale = (int) scale;
                     imageWidth *= intScale;
                     imageHeight *= intScale;
                     drawImage = (imageWidth > 0 && imageHeight > 0)
                           ? displayImage.getScaledInstance(imageWidth, imageHeight, Image.SCALE_DEFAULT) : null;
                  }
               }
               String text = displaySizeText;
               if (scaled) {
                  text += " (shown at " + imageWidth + " x " + imageHeight + ")";
               }
               if (!isComplete) {
                  text += " (image is currently incomplete)";
               }
               setDisplayText(text);
            
               int xOffs = borderXOffs + (width - (int) (imageWidth / sx)) / 2;
               int yOffs = borderYOffs + (height - (int) (imageHeight / sy)) / 2;
               if (drawImage != null) {
                  PaintUtil.drawPhysicalImage((Graphics2D) g, drawImage, xOffs, yOffs);
               }
            }
         };
      imagePanel.getAccessibleContext().setAccessibleName(getViewName() + " viewer for " + getVIData().getName());
      Util.makeFocusable(imagePanel, true);
   }


   /** Sets the display text.
    *
    * @param sizeText text description of the size. **/
   private void setDisplayText(final String sizeText) {
      String text = (sizeText == null) ? "" : sizeText;
      String ccText = currentClientText == null ? "" : currentClientText;
      if (!text.isEmpty() && !ccText.isEmpty()) {
         text += "  ";
      }
      text += ccText;
      if (text.isEmpty()) {
         text = " ";
      }
      displayLabel.setText(text);
   }


    /** {@inheritDoc} **/
   @Override
   public void updateState(final ViewerValueData valueData, final ViewerUpdateData data, final DebugContext context)
         throws ViewerException {
      currentData = getData(valueData, data, context);
   }


   /** Gets the image data. This is called by the debugger when the value being viewed may have changed or a watched
    * event has occurred. This will be called from the debugger thread.
    *
    * @param valueData the new value and associated information, such as declared type. This will be null for
    * animation updates.
    *
    * @param data information about this update, such as why it was triggered, and if it was triggered by a flagged
    * method entry, the method argument values.
    *
    * @param context debugger context that is necessary for working with values, and provides some global debugger
    * access. This will be null for animation updates.
    *
    * @return the new image data.
    *
    * @throws ViewerException if an exception is encountered. **/
   public abstract Data getData(final ViewerValueData valueData, final ViewerUpdateData data,
         final DebugContext context) throws ViewerException;


   /** Adds items to the settings panel. The default implementation does nothing.
    *
    * @param panel the settings panel.
    *
    * @param constraints the grid bag constraints. Insets will be set to a standard spacing and weightx to .0001. The
    * standard spacing should be used at the top and no spacing on the bottom of the items that are added. All
    * constraints should be restored before this call returns. **/
   public void addSettingsItems(final JPanel panel, final GridBagLayout gbl, final GridBagConstraints constraints) {
   }


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


   /** {@inheritDoc} **/
   @Override
   public boolean toXML(final Document doc, final Element e) {
      if (mode == initMode) {
         return false;
      }
      XMLUtil.addXMLString(doc, e, "Mode", mode.name());
      return true;
   }
   
   
   /** Converts pixel values to ARGB values. The default implementation does not modify the values.
    *
    * @param pixels the pixel values.
    *
    * @return the ARGB values to be displayed. **/
   public int[] convertPixels(final int[] pixels) {
      return pixels;
   }
}
