package org.basex.gui.view.map;

import static org.basex.core.Text.*;
import static org.basex.gui.GUIConstants.*;
import static org.basex.gui.layout.BaseXKeys.*;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.event.ComponentEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.awt.image.BufferedImage;
import javax.swing.SwingUtilities;
import org.basex.data.Data;
import org.basex.data.Nodes;
import org.basex.gui.GUIProp;
import org.basex.gui.layout.BaseXLayout;
import org.basex.gui.layout.BaseXPopup;
import org.basex.gui.view.View;
import org.basex.gui.view.ViewData;
import org.basex.gui.view.ViewNotifier;
import org.basex.util.Performance;
import org.basex.util.Token;
import org.basex.util.ft.FTLexer;
import org.basex.util.list.IntList;
import org.basex.util.list.TokenList;

/**
 * This view is a TreeMap implementation.
 *
 * @author BaseX Team 2005-11, BSD License
 * @author Christian Gruen
 * @author Joerg Hauser
 * @author Bastian Lemke
 */
public final class MapView extends View implements Runnable {
  /** Dynamic zooming steps. */
  private static final int[] ZS = { 0, 0, 0, 0, 20, 80, 180, 320, 540, 840,
      1240, 1740, 2380, 3120, 4000, 4980, 5980, 6860, 7600, 8240, 8740, 9140,
      9440, 9660, 9800, 9900, 9960, 9980, 9980, 9980, 10000 };
  /** Number of zooming steps. */
  private static final int ZOOMSIZE = ZS.length - 1;
  /** Maximum zooming step. */
  private static final int MAXZS = ZS[ZOOMSIZE];

  /** Array of current rectangles. */
  private MapRects mainRects;
  /** Data specific map layout. */
  private transient MapPainter painter;
  /** Text lengths. */
  private int[] textLen;

  /** Rectangle history. */
  private final MapRect[] rectHist = new MapRect[ViewNotifier.MAXHIST];
  /** Current zooming Step (set to 0 when no zooming takes place). */
  private int zoomStep;
  /** Main rectangle. */
  private MapRect mainRect;
  /** Dragged rectangle. */
  private MapRect selBox;
  /** Flag for zooming in/out. */
  private boolean zoomIn;
  /** Zooming speed. */
  private int zoomSpeed;

  /** Horizontal mouse position. */
  private int mouseX = -1;
  /** Vertical mouse position. */
  private int mouseY = -1;
  /** Drag tolerance. */
  private int dragTol;

  /** Currently focused rectangle. */
  private MapRect focused;

  /** TreeMap. */
  private BufferedImage mainMap;
  /** Zoomed TreeMap. */
  private BufferedImage zoomMap;

  /** Keeps the whole map layout. */
  MapLayout layout;

  /**
   * Default constructor.
   * @param man view manager
   */
  public MapView(final ViewNotifier man) {
    super(MAPVIEW, HELPMAP, man);
    new BaseXPopup(this, POPUP);
  }

  /**
   * Creates a buffered image.
   * @return buffered image
   */
  private BufferedImage createImage() {
    return new BufferedImage(Math.max(1, getWidth()), Math.max(1, getHeight()),
        BufferedImage.TYPE_INT_BGR);
  }

  @Override
  public void refreshInit() {
    if(painter != null) painter.close();
    painter = null;
    mainRects = null;
    focused = null;
    textLen = null;
    zoomStep = 0;

    final Data data = gui.context.data();
    final GUIProp gprop = gui.gprop;
    if(data != null && visible()) {
      painter = new MapDefault(this, gprop);
      mainMap = createImage();
      zoomMap = createImage();
      refreshLayout();
    }
  }

  @Override
  public void refreshFocus() {
    final int f = gui.context.focused;
    if(f == -1) focused = null;

    if(mainRects == null) return;
    final int ms = mainRects.size;
    for(int mi = 0; mi < ms; ++mi) {
      final MapRect rect = mainRects.get(mi);
      if(f == rect.pre || mi + 1 == ms || f < mainRects.get(mi + 1).pre) {
        focused = rect;
        repaint();
        break;
      }
    }
  }

  @Override
  public void refreshMark() {
    drawMap(mainMap, mainRects, 1f);
    repaint();
  }

  @Override
  public void refreshContext(final boolean more, final boolean quick) {
    // use simple zooming animation for result node filtering
    final Nodes context = gui.context.current();
    final int hist = gui.notify.hist;
    final boolean page = !more && rectHist[hist + 1] != null &&
      rectHist[hist + 1].pre == 0 || more && (context.size() != 1 ||
      focused == null || context.list[0] != focused.pre);
    if(page) focused = new MapRect(0, 0, getWidth(), 1);

    zoom(more, quick);
  }

  @Override
  public void refreshLayout() {
    if(painter == null) return;

    // calculate map
    calc(new MapRect(0, 0, getWidth(), getHeight(), 0, 0),
        gui.context.current(), mainMap);
    repaint();
  }

  @Override
  public void refreshUpdate() {
    textLen = null;
    refreshContext(false, true);
  }

  @Override
  public boolean visible() {
    return gui.gprop.is(GUIProp.SHOWMAP);
  }

  @Override
  public void visible(final boolean v) {
    gui.gprop.set(GUIProp.SHOWMAP, v);
  }

  @Override
  protected boolean db() {
    return true;
  }

  /**
   * Zooms the focused rectangle.
   * @param more show more
   * @param quick context switch (no animation)
   */
  private void zoom(final boolean more, final boolean quick) {
    gui.updating = !quick;
    zoomIn = more;

    // choose zooming rectangle
    final int hist = gui.notify.hist;
    if(more) {
      rectHist[hist] = focused;
      mainRect = rectHist[hist];
    } else {
      mainRect = rectHist[hist + 1];
    }
    if(mainRect == null) mainRect = new MapRect(0, 0, getWidth(), getHeight());

    // reset data & start zooming
    final BufferedImage tmpMap = zoomMap;
    zoomMap = mainMap;
    mainMap = tmpMap;
    focused = null;

    // create new context nodes
    refreshLayout();

    // calculate zooming speed (slower for large zooming scales)
    if(mainRect.w > 0 && mainRect.h > 0) {
      zoomSpeed = (int) (Math.log(64d * getWidth() / mainRect.w) +
          Math.log(64d * getHeight() / mainRect.h));
    }

    if(quick) {
      gui.updating = false;
      focus();
      repaint();
    } else {
      zoomStep = ZOOMSIZE;
      new Thread(this).start();
    }
  }

  @Override
  public void run() {
    focused = null;

    // run zooming
    while(zoomStep > 1) {
      Performance.sleep(zoomSpeed);
      --zoomStep;
      repaint();
    }
    // wait until current painting is finished
    while(gui.painting) Performance.sleep(zoomSpeed);

    // remove old rectangle and repaint map
    zoomStep = 0;
    gui.updating = false;
    focus();
    repaint();
  }

  /**
   * Finds the rectangle at the cursor position.
   * @return focused rectangle
   */
  private boolean focus() {
    if(gui.updating || mainRects == null) return false;

    /*
     * Loop through all rectangles. As the rectangles are sorted by pre order
     * and small rectangles are descendants of bigger ones, the focused
     * rectangle can be found by simply parsing the array backwards.
     */
    int r = mainRects.size;
    while(--r >= 0) {
      final MapRect rect = mainRects.get(r);
      if(rect.contains(mouseX, mouseY)) break;
    }
    // don't focus top rectangles
    final MapRect fr = r >= 0 ? mainRects.get(r) : null;

    // find focused rectangle
    final boolean nf = focused != fr || fr != null && fr.thumb;
    focused = fr;

    if(fr != null) gui.cursor(painter.mouse(focused, mouseX, mouseY, false) ?
        CURSORHAND : CURSORARROW);

    if(nf) gui.notify.focus(focused != null ? focused.pre : -1, this);
    return nf;
  }

  /**
   * Initializes the calculation of the main map.
   * @param rect initial space to layout rectangles in
   * @param nodes nodes to draw in the map
   * @param map image to draw rectangles on
   */
  private void calc(final MapRect rect, final Nodes nodes,
      final BufferedImage map) {

    // calculate new main rectangles
    gui.cursor(CURSORWAIT);

    initLen();
    layout = new MapLayout(nodes.data, textLen, gui.gprop);
    layout.makeMap(rect, new MapList(nodes.list.clone()),
        0, (int) nodes.size() - 1);
    // rectangles are copied to avoid synchronization issues
    mainRects = layout.rectangles.copy();

    painter.init(mainRects);
    drawMap(map, mainRects, 1f);
    focus();
    /*
     * Screenshots: try { File file = new File("screenshot.png");
     * ImageIO.write(mainMap, "png", file); } catch(IOException ex) {
     * Util.stack(ex); }
     */
    gui.cursor(CURSORARROW, true);
  }

  @Override
  public void paintComponent(final Graphics g) {
    final Data data = gui.context.data();
    if(data == null) return;

    if(mainRects == null || mainRects.size == 0 || mainRects.get(0).w == 0) {
      super.paintComponent(g);
      if(mainRects == null || mainRects.size != 0) refreshInit();
      return;
    }

    // calculate map
    gui.painting = true;

    // paint map
    final boolean in = zoomStep > 0 && zoomIn;
    final Image img1 = in ? zoomMap : mainMap;
    final Image img2 = in ? mainMap : zoomMap;
    if(zoomStep > 0) {
      drawImage(g, img1, -zoomStep);
      drawImage(g, img2, zoomStep);
    } else {
      drawImage(g, mainMap, zoomStep);
    }

    // check if focused rectangle is valid
    if(focused != null && focused.pre >= data.meta.size) focused = null;

    // skip node path view
    final MapRect f = focused;
    if(f == null || mainRects.size == 1 && f == mainRects.get(0)) {
      gui.painting = false;
      if(f == null || !f.thumb) return;
    }

    final GUIProp gprop = gui.gprop;
    if(gprop.num(GUIProp.MAPOFFSETS) == 0) {
      g.setColor(color(32));
      int pre = mainRects.size;
      int par = ViewData.parent(data, f.pre);
      while(--pre >= 0) {
        final MapRect rect = mainRects.get(pre);
        if(rect.pre == par) {
          final int x = rect.x;
          final int y = rect.y;
          final int w = rect.w;
          final int h = rect.h;
          g.drawRect(x, y, w, h);
          g.drawRect(x - 1, y - 1, w + 2, h + 2);
          par = ViewData.parent(data, par);
        }
      }
    }

    if(selBox != null) {
      g.setColor(colormark3);
      g.drawRect(selBox.x, selBox.y, selBox.w, selBox.h);
      g.drawRect(selBox.x - 1, selBox.y - 1, selBox.w + 2, selBox.h + 2);
    } else {
      // paint focused rectangle
      final int x = f.x;
      final int y = f.y;
      final int w = f.w;
      final int h = f.h;
      g.setColor(color5);
      g.drawRect(x, y, w, h);
      g.drawRect(x + 1, y + 1, w - 2, h - 2);

      // draw tag label
      g.setFont(font);
      smooth(g);
      if(data.kind(f.pre) == Data.ELEM) {
        String tt = Token.string(ViewData.tag(gprop, data, f.pre));
        if(tt.length() > 32) tt = tt.substring(0, 30) + DOTS;
        BaseXLayout.drawTooltip(g, tt, x, y, getWidth(), f.level + 5);
      }

      if(f.thumb) {
        // draw tooltip for thumbnail
        f.x += 3;
        f.w -= 3;
        // read content from disk
        final byte[] text = painter.content(data, f);
        // calculate tooltip
        final int[][] info = new FTLexer().init(text).info();
        final TokenList tl = MapRenderer.calculateToolTip(f,
            info, mouseX, mouseY, getWidth(), g);
        final MapRect mr = new MapRect(getX(), getY(), getWidth(), getHeight());
        // draw calculated tooltip
        MapRenderer.drawToolTip(g, mouseX, mouseY, mr, tl,
            gprop.num(GUIProp.FONTSIZE));
        f.x -= 3;
        f.w += 3;
      }
    }
    gui.painting = false;
  }

  /**
   * Draws image with correct scaling.
   * @param g graphics reference
   * @param img image to be drawn
   * @param zi zooming factor
   */
  private void drawImage(final Graphics g, final Image img, final int zi) {
    if(img == null) return;
    final MapRect r = new MapRect(0, 0, getWidth(), getHeight());
    zoom(r, zi);
    g.drawImage(img, r.x, r.y, r.x + r.w, r.y + r.h, 0, 0, getWidth(),
        getHeight(), this);
  }

  /**
   * Zooms the coordinates of the specified rectangle.
   * @param r rectangle to be zoomed
   * @param zs zooming step
   */
  private void zoom(final MapRect r, final int zs) {
    int xs = r.x;
    int ys = r.y;
    int xe = xs + r.w;
    int ye = ys + r.h;

    // calculate zooming rectangle
    // get window size
    if(zs != 0) {
      final MapRect zr = mainRect;
      final int tw = getWidth();
      final int th = getHeight();
      if(zs > 0) {
        final long s = zoomIn ? ZS[zs] : ZS[ZOOMSIZE - zs];
        xs = (int) ((zr.x + xs * zr.w / tw - xs) * s / MAXZS);
        ys = (int) ((zr.y + ys * zr.h / th - ys) * s / MAXZS);
        xe += (int) ((zr.x + xe * zr.w / tw - xe) * s / MAXZS);
        ye += (int) ((zr.y + ye * zr.h / th - ye) * s / MAXZS);
      } else {
        final long s = 10000 - (zoomIn ? ZS[-zs] : ZS[ZOOMSIZE + zs]);
        if(zr.w == 0) zr.w = 1;
        if(zr.h == 0) zr.h = 1;
        xs = (int) (-xe * zr.x / zr.w * s / MAXZS);
        xe = (int) (xs + xe + xe * (xe - zr.w) / zr.w * s / MAXZS);
        ys = (int) (-ye * zr.y / zr.h * s / MAXZS);
        ye = (int) (ys + ye + ye * (ye - zr.h) / zr.h * s / MAXZS);
      }
    }
    r.x = xs;
    r.y = ys;
    r.w = xe - xs;
    r.h = ye - ys;
  }

  /**
   * Creates a buffered image for the treemap.
   * @param map Image to draw the map on
   * @param rects calculated rectangles
   * @param sc scale the rectangles
   */
  private void drawMap(final BufferedImage map, final MapRects rects,
      final float sc) {
    final Graphics g = map.getGraphics();
    smooth(g);
    painter.drawRectangles(g, rects, sc);
  }

  @Override
  public void mouseMoved(final MouseEvent e) {
    if(gui.updating) return;
    mouseX = e.getX();
    mouseY = e.getY();
    if(focus()) repaint();
  }

  @Override
  public void mouseClicked(final MouseEvent e) {
    if(gui.updating) return;
    // process mouse clicks by the specific painter
    if(SwingUtilities.isLeftMouseButton(e) && focused != null)
      painter.mouse(focused, mouseX, mouseY, true);
  }

  @Override
  public void mousePressed(final MouseEvent e) {
    if(gui.updating) return;
    super.mousePressed(e);

    mouseX = e.getX();
    mouseY = e.getY();
    dragTol = 0;
    if(!focus() && gui.context.focused == -1) return;

    // add or remove marked node
    final Nodes marked = gui.context.marked;
    if(e.getClickCount() == 2) {
      if(mainRects.size != 1) gui.notify.context(marked, false, null);
    } else if(e.isShiftDown()) {
      gui.notify.mark(1, null);
    } else if(sc(e) && SwingUtilities.isLeftMouseButton(e)) {
      gui.notify.mark(2, null);
    } else {
      if(!marked.contains(gui.context.focused)) gui.notify.mark(0, null);
    }
  }

  @Override
  public void mouseDragged(final MouseEvent e) {
    if(gui.updating || ++dragTol < 8 || mainRects.sorted != mainRects.list)
      return;

    // refresh mouse focus
    int mx = mouseX;
    int my = mouseY;
    int mw = e.getX() - mx;
    int mh = e.getY() - my;
    if(mw < 0) mx -= mw = -mw;
    if(mh < 0) my -= mh = -mh;
    selBox = new MapRect(mx, my, mw, mh);

    final Data data = gui.context.data();
    final IntList il = new IntList();
    int np = 0;
    final int rl = mainRects.size;
    for(int r = 0; r < rl; ++r) {
      final MapRect rect = mainRects.get(r);
      if(mainRects.get(r).pre < np) continue;
      if(selBox.contains(rect)) {
        il.add(rect.pre);
        np = rect.pre + ViewData.size(data, rect.pre);
      }
    }
    gui.notify.mark(new Nodes(il.toArray(), data), null);
  }

  @Override
  public void mouseReleased(final MouseEvent e) {
    if(gui.updating) return;
    if(selBox != null) {
      selBox = null;
      repaint();
    }
  }

  @Override
  public void mouseWheelMoved(final MouseWheelEvent e) {
    if(gui.updating || gui.context.focused == -1) return;
    if(e.getWheelRotation() <= 0) {
      final Nodes m = new Nodes(gui.context.focused, gui.context.data());
      gui.context.marked = m;
      gui.notify.context(m, false, null);
    } else {
      gui.notify.hist(false);
    }
  }

  @Override
  public void keyPressed(final KeyEvent e) {
    super.keyPressed(e);
    if(gui.updating || mainRects == null || control(e)) return;

    final boolean cursor = PREVLINE.is(e) || NEXTLINE.is(e) ||
        PREV.is(e) || NEXT.is(e);
    if(!cursor) return;

    if(focused == null) focused = mainRects.get(0);

    final int fs = gui.gprop.num(GUIProp.FONTSIZE);
    int o = fs + 4;
    final boolean shift = e.isShiftDown();
    if(PREVLINE.is(e)) {
      mouseY = focused.y + (shift ? focused.h - fs : 0) - 1;
      if(shift) mouseX = focused.x + (focused.w >> 1);
    } else if(NEXTLINE.is(e)) {
      mouseY = focused.y + (shift ? o : focused.h + 1);
      if(shift) mouseX = focused.x + (focused.w >> 1);
    } else if(PREV.is(e)) {
      mouseX = focused.x + (shift ? focused.w - fs : 0) - 1;
      if(shift) mouseY = focused.y + (focused.h >> 1);
    } else if(NEXT.is(e)) {
      mouseX = focused.x + (shift ? o : focused.w + 1);
      if(shift) mouseY = focused.y + (focused.h >> 1);
    }

    o = mainRects.get(0).w == getWidth() ? (o >> 1) + 1 : 0;
    mouseX = Math.max(o, Math.min(getWidth() - o - 1, mouseX));
    mouseY = Math.max(o << 1, Math.min(getHeight() - o - 1, mouseY));

    if(focus()) repaint();
  }

  @Override
  public void componentResized(final ComponentEvent e) {
    if(gui.updating) return;
    focused = null;
    mainMap = createImage();
    zoomMap = createImage();
    refreshLayout();
  }

  /**
   * Initializes the text lengths and stores them into an array.
   */
  private void initLen() {
    painter.reset();

    final Data data = gui.context.data();
    if(textLen != null || gui.gprop.num(GUIProp.MAPWEIGHT) == 0) return;

    final int size = data.meta.size;
    textLen = new int[size];

    final IntList pars = new IntList();
    int l = 0;

    for(int pre = 0; pre < size; ++pre) {
      final int kind = data.kind(pre);
      final int par = data.parent(pre, kind);

      final int ll = l;
      while(l > 0 && pars.get(l - 1) > par) {
        textLen[pars.get(l - 1)] += textLen[pars.get(l)];
        --l;
      }
      if(l > 0 && ll != l) textLen[pars.get(l - 1)] += textLen[pars.get(l)];
      pars.set(l, pre);

      if(kind == Data.DOC || kind == Data.ELEM) {
        pars.set(++l, 0);
      } else {
        textLen[pre] = data.textLen(pre, kind != Data.ATTR);
      }
    }
    while(--l >= 0) textLen[pars.get(l)] += textLen[pars.get(l + 1)];
  }
}
