/*
 * {{{ header & license
 * Copyright (c) 2008 elbart0 at free.fr (submitted via email)
 *
 * This program 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 program 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 program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 * }}}
 */

import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xhtmlrenderer.extend.FSImage;
import org.xhtmlrenderer.extend.ReplacedElement;
import org.xhtmlrenderer.extend.UserAgentCallback;
import org.xhtmlrenderer.layout.LayoutContext;
import org.xhtmlrenderer.render.BlockBox;
import org.xhtmlrenderer.swing.AWTFSImage;
import org.xhtmlrenderer.swing.SwingReplacedElement;
import org.xhtmlrenderer.swing.SwingReplacedElementFactory;
import org.xhtmlrenderer.util.XRLog;

import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionAdapter;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Rectangle2D;
import java.util.*;
import java.util.logging.Level;


/**
 * Sample for handling image maps in XHTML, as replaced elements.
 *
 * Sample is incomplete in current state and meant as a starting point for future work.
 */
public class ImageMapReplacedElementFactory extends SwingReplacedElementFactory {
   private final ImageMapListener listener;
   private static final String IMG_USEMAP_ATTR = "usemap";
   private static final String MAP_ELT = "map";
   private static final String MAP_NAME_ATTR = "name";
   private static final String AREA_ELT = "area";
   private static final String AREA_SHAPE_ATTR = "shape";
   private static final String AREA_COORDS_ATTR = "coords";
   private static final String AREA_HREF_ATTR = "href";
   private static final String RECT_SHAPE = "rect";
   private static final String RECTANGLE_SHAPE = "rectangle";
   private static final String CIRC_SHAPE = "circ";
   private static final String CIRCLE_SHAPE = "circle";
   private static final String POLY_SHAPE = "poly";
   private static final String POLYGON_SHAPE = "polygon";

   public ImageMapReplacedElementFactory(ImageMapListener listener) {
      if (null == listener) {
         throw new IllegalArgumentException("listener required");
      }
      this.listener = listener;
   }

   public ReplacedElement createReplacedElement(LayoutContext context, BlockBox box, UserAgentCallback uac, int cssWidth, int cssHeight) {
      Element e = box.getElement();
      if (e == null) {
         return null;
      } else if (context.getNamespaceHandler().isImageElement(e)) {
         final String usemapAttr = context.getNamespaceHandler().getAttributeValue(e, IMG_USEMAP_ATTR);
         if (isNotBlank(usemapAttr)) {
            final ReplacedElement re = replaceImageMap(uac, context, e, usemapAttr, cssWidth, cssHeight);
            if (context.isInteractive() && re instanceof SwingReplacedElement) {
                context.getCanvas().add(((SwingReplacedElement) re).getJComponent());
            }
            return re;
         } else {
            return replaceImage(uac, context, e, cssWidth, cssHeight);
         }
      } else {
         return null;
      }
   }

    private boolean isNotBlank(String _v) {
        if (_v == null || _v.length() == 0) {
            return false;
        }
        for (int i = 0; i < _v.length(); i++) {
             if (Character.isWhitespace(_v.charAt(i))) continue;
            return false;
        }
        return true;
    }

    // See SwingReplacedElementFactory#replaceImage
   protected ReplacedElement replaceImageMap(UserAgentCallback uac, LayoutContext context, Element elem, String usemapAttr, int cssWidth, int cssHeight) {
      ReplacedElement re;
      // lookup in cache, or instantiate
      re = lookupImageReplacedElement(elem);
      if (re == null) {
         Image im = null;
         String imageSrc = context.getNamespaceHandler().getImageSourceURI(elem);
         if (imageSrc == null || imageSrc.length() == 0) {
            XRLog.layout(Level.WARNING, "No source provided for img element.");
            re = newIrreplaceableImageElement(cssWidth, cssHeight);
         } else {
            FSImage fsImage = uac.getImageResource(imageSrc).getImage();
            if (fsImage != null) {
               im = ((AWTFSImage) fsImage).getImage();
            }

            if (im != null) {
               final String mapName = usemapAttr.substring(1);
               Node map = elem.getOwnerDocument().getElementById(mapName);
               if (null == map) {
                  final NodeList maps = elem.getOwnerDocument().getElementsByTagName(MAP_ELT);
                  for (int i = 0; i < maps.getLength(); i++) {
                      String mapAttr = ImageMapReplacedElement.getAttribute(maps.item(i).getAttributes(), MAP_NAME_ATTR);
                      if (areEqual(mapName, mapAttr)) {
                        map = maps.item(i);
                        break;
                     }
                  }
                  if (null == map) {
                     XRLog.layout(Level.INFO, "No map named: '" + mapName + "'");
                  }
               }
               re = new ImageMapReplacedElement(im, map, cssWidth, cssHeight, listener);
            } else {
               re = newIrreplaceableImageElement(cssWidth, cssHeight);
            }
         }
         storeImageReplacedElement(elem, re);
      }
      return re;
   }

    private static boolean areEqual(String str1, String str2) {
        return (str1 == null && str2 == null) || (str1 != null && str1.equals(str2));
    }
    private static boolean areEqualIgnoreCase(String str1, String str2) {
        return (str1 == null && str2 == null) || (str1 != null && str1.equalsIgnoreCase(str2));
    }

    private static class ImageMapReplacedElement extends SwingReplacedElement {
      private final Map areas;

      public ImageMapReplacedElement(Image image, Node map, int targetWidth, int targetHeight, final ImageMapListener listener) {
         super(create(image, targetWidth, targetHeight));
         areas = parseMap(map);
         getJComponent().addMouseListener(new MouseAdapter() {
            public void mouseClicked(MouseEvent e) {
               final Point point = e.getPoint();
                final Set set = areas.entrySet();
                for (Iterator iterator = set.iterator(); iterator.hasNext();) {
                    Map.Entry entry = (Map.Entry) iterator.next();

                    if (((Shape) entry.getKey()).contains(point)) {
                        listener.areaClicked(new ImageMapEvent(this, (String) entry.getValue()));
                    }
                }
            }

            public void mouseExited(MouseEvent e) {
               getJComponent().setCursor(Cursor.getDefaultCursor());
            }
         });
         getJComponent().addMouseMotionListener(new MouseMotionAdapter() {
            public void mouseMoved(MouseEvent e) {
               final JComponent c = getJComponent();
               final Point point = e.getPoint();
                final Set set = areas.entrySet();
                for (Iterator iterator = set.iterator(); iterator.hasNext();) {
                    Map.Entry entry = (Map.Entry) iterator.next();

                    if (((Shape) entry.getKey()).contains(point)) {
                        updateCursor(c, Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
                        return;
                    }
                }
               updateCursor(c, Cursor.getDefaultCursor());
            }
         });
      }

      private static void updateCursor(JComponent c, Cursor cursor) {
         if (!c.getCursor().equals(cursor)) {
            c.setCursor(cursor);
         }
      }

      private static Map parseMap(Node map) {
         if (null == map) {
            return Collections.emptyMap();
         } else if (map.hasChildNodes()) {
            final NodeList children = map.getChildNodes();
            final Map areas = new HashMap(children.getLength());
            for (int i = 0; i < children.getLength(); i++) {
               final Node area = children.item(i);
               if (areEqualIgnoreCase(AREA_ELT, area.getNodeName())) {
                  if (area.hasAttributes()) {
                     final NamedNodeMap attrs = area.getAttributes();
                     final String shapeAttr = getAttribute(attrs, AREA_SHAPE_ATTR);
                     final String[] coords = getAttribute(attrs, AREA_COORDS_ATTR).split(",");
                     final String href = getAttribute(attrs, AREA_HREF_ATTR);
                     if (areEqualIgnoreCase(RECT_SHAPE, shapeAttr) || areEqualIgnoreCase(RECTANGLE_SHAPE, shapeAttr)) {
                        final Shape shape = getCoords(coords, 4);
                        if (null != shape) {
                           areas.put(shape, href);
                        }
                     } else if (areEqualIgnoreCase(CIRC_SHAPE, shapeAttr) || areEqualIgnoreCase(CIRCLE_SHAPE, shapeAttr)) {
                        final Shape shape = getCoords(coords, 3);
                        if (null != shape) {
                           areas.put(shape, href);
                        }
                     } else if (areEqualIgnoreCase(POLY_SHAPE, shapeAttr) || areEqualIgnoreCase(POLYGON_SHAPE, shapeAttr)) {
                        final Shape shape = getCoords(coords, -1);
                        if (null != shape) {
                           areas.put(shape, href);
                        }
                     } else {
                        if (XRLog.isLoggingEnabled()) {
                           XRLog.layout(Level.INFO, "Unsupported shape: '" + shapeAttr + "'");
                        }
                     }
                  }
               }
            }
            return areas;
         } else {
            return Collections.emptyMap();
         }
      }

      private static String getAttribute(NamedNodeMap attrs, String attrName) {
         final Node node = attrs.getNamedItem(attrName);
         return null == node ? null : node.getNodeValue();
      }

      private static Shape getCoords(String[] coordValues, int length) {
         if ((-1 == length && 0 == coordValues.length % 2) || length == coordValues.length) {
            int[] coords = new int[coordValues.length];
            int i = 0;
             for (int i1 = 0; i1 < coordValues.length; i1++) {
                 String coord = coordValues[i1];
                 try {
                     coords[i++] = Integer.parseInt(coord.trim());
                 } catch (NumberFormatException e) {
                     XRLog.layout(Level.WARNING, "Error while parsing shape coords", e);
                     return null;
                 }
             }
            if (4 == length) {
               return new Rectangle2D.Float(coords[0], coords[1], coords[2] - coords[0], coords[3] - coords[1]);
            } else if (3 == length) {
               final int radius = coords[2];
               return new Ellipse2D.Float(coords[0] - radius, coords[1] - radius, radius * 2, radius * 2);
            } else if (-1 == length) {
               final int npoints = coords.length / 2;
               final int[] xpoints = new int[npoints];
               final int[] ypoints = new int[npoints];
               for (int c = 0, p = 0; p < npoints; p++) {
                  xpoints[p] = coords[c++];
                  ypoints[p] = coords[c++];
               }
               return new Polygon(xpoints, ypoints, npoints);
            } else {
               XRLog.layout(Level.INFO, "Unsupported shape: '" + length + "'");
               return null;
            }
         } else {
            return null;
         }
      }

      private static JComponent create(Image image, int targetWidth, int targetHeight) {
         final JLabel component = new JLabel(new ImageIcon(image));
         component.setSize(component.getPreferredSize());
         return component;
      }
   }
}