/*
 * File    : TextGrid.java
 * Created : 10-sep-2001 13:08
 * By      : fbusquets
 *
 * JClic - Authoring and playing system for educational activities
 *
 * Copyright (C) 2000 - 2018 Francesc Busquets & Departament
 * d'Educacio de la Generalitat de Catalunya
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 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 General Public License for more details (see the LICENSE file).
 */

package edu.xtec.jclic.boxes;

import edu.xtec.util.StrUtils;
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.Stroke;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphVector;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.geom.Rectangle2D.Double;
import java.awt.image.ImageObserver;
import javax.swing.JComponent;
import javax.swing.RepaintManager;
import javax.swing.Timer;

/**
 * This class is a special {@link edu.xtec.jclic.boxes.AbstractBox} that
 * displays a grid of single characters. It is used in activities like
 * crosswords and scrambled letters.
 *
 * @author Francesc Busquets (fbusquets@xtec.cat)
 * @version 13.08.08
 */
public class TextGrid extends AbstractBox implements Cloneable, Resizable, ActionListener {

  int nRows, nCols;
  char[][] chars;
  char[][] answers;
  int[][] attributes;
  double cellWidth;
  double cellHeight;
  Rectangle2D preferredBounds = new Double();
  public char wild = TextGridContent.DEFAULT_WILD;
  String randomChars = TextGridContent.DEFAULT_RANDOM_CHARS;
  boolean cursorEnabled;
  boolean useCursor;
  Point cursor = new Point();
  boolean cursorBlink;
  Timer cursorTimer;
  boolean wildTransparent;

  public static final int MIN_CELL_SIZE = 12, DEFAULT_CELL_SIZE = 20, MIN_INTERNAL_MARGIN = 2;
  public static final int NORMAL = 0, INVERTED = 1, HIDDEN = 2, LOCKED = 4, MARKED = 8, TRANSPARENT = 16;

  /** Creates new TextGridBox */
  public TextGrid(AbstractBox parent, JComponent container, double setX, double setY, int setNcw, int setNch,
      double setCellW, double setCellH, BoxBase boxBase, boolean setBorder) {
    super(parent, container, boxBase);
    x = setX;
    y = setY;
    nCols = Math.max(1, setNcw);
    nRows = Math.max(1, setNch);
    cellWidth = Math.max(setCellW, MIN_CELL_SIZE);
    cellHeight = Math.max(setCellH, MIN_CELL_SIZE);
    width = cellWidth * nCols;
    height = cellHeight * nRows;
    chars = new char[nRows][nCols];
    attributes = new int[nRows][nCols];
    preferredBounds.setRect(getBounds());
    setBorder(setBorder);
    cursorTimer = new Timer(500, this);
    cursorTimer.setRepeats(true);
    cursorEnabled = false;
    useCursor = false;
    wildTransparent = false;
    answers = null;
  }

  public static TextGrid createEmptyGrid(AbstractBox parent, JComponent container, double setX, double setY,
      TextGridContent tgc, boolean wildTransparent) {
    TextGrid result = new TextGrid(parent, container, setX, setY, tgc.ncw, tgc.nch, tgc.w, tgc.h, tgc.bb, tgc.border);
    result.wild = tgc.wild;
    result.randomChars = tgc.randomChars;
    result.wildTransparent = wildTransparent;
    return result;
  }

  public void setChars(String[] text) {
    answers = new char[nRows][nCols];
    for (int py = 0; py < nRows; py++) {
      String line = py < text.length ? text[py] : null;
      for (int px = 0; px < nCols; px++) {
        chars[py][px] = (line == null || px >= line.length()) ? ' ' : line.charAt(px);
        answers[py][px] = chars[py][px];
      }
    }
    repaint();
  }

  public void randomize() {
    for (int py = 0; py < nRows; py++)
      for (int px = 0; px < nCols; px++)
        if (chars[py][px] == wild)
          chars[py][px] = randomChars.charAt((int) (Math.random() * randomChars.length()));
    repaint();
  }

  public void setCellAttributes(boolean lockWild, boolean clearChars) {
    int atr = LOCKED;
    if (wildTransparent)
      atr |= TRANSPARENT;
    else
      atr |= INVERTED | HIDDEN;
    for (int py = 0; py < nRows; py++)
      for (int px = 0; px < nCols; px++)
        if (lockWild && chars[py][px] == wild)
          attributes[py][px] = atr;
        else {
          attributes[py][px] = NORMAL;
          if (clearChars)
            chars[py][px] = ' ';
        }
    repaint();
  }

  public void setCellLocked(int px, int py, boolean locked) {
    if (px >= 0 && px < nCols && py >= 0 && py < nRows) {
      attributes[py][px] = locked ? LOCKED | (wildTransparent ? TRANSPARENT : INVERTED | HIDDEN) : NORMAL;
    }
  }

  public Point getItemFor(int rx, int ry) {
    if (!isValidCell(rx, ry))
      return null;
    Point point = new Point();
    boolean inBlack = false, startCount = false;
    for (int px = 0; px < rx; px++) {
      if ((attributes[ry][px] & LOCKED) != 0) {
        if (!inBlack) {
          if (startCount)
            point.x++;
          inBlack = true;
        }
      } else {
        startCount = true;
        inBlack = false;
      }
    }
    inBlack = false;
    startCount = false;
    for (int py = 0; py < ry; py++) {
      if ((attributes[py][rx] & LOCKED) != 0) {
        if (!inBlack) {
          if (startCount)
            point.y++;
          inBlack = true;
        }
      } else {
        startCount = true;
        inBlack = false;
      }
    }
    return point;
  }

  public void setCursorEnabled(boolean status) {
    cursorEnabled = status;
    if (status == true)
      startCursorBlink();
    else
      stopCursorBlink();
  }

  public void startCursorBlink() {
    if (useCursor && cursorEnabled && cursorTimer != null && !cursorTimer.isRunning()) {
      blink(1);
      cursorTimer.start();
    }
  }

  public void stopCursorBlink() {
    if (cursorTimer != null && cursorTimer.isRunning()) {
      cursorTimer.stop();
      blink(-1);
    }
  }

  public void moveCursor(int dx, int dy, boolean skipLocked) {
    if (useCursor) {
      Point point = findNextCellWithAttr(cursor.x, cursor.y, skipLocked ? LOCKED : NORMAL, dx, dy, false);
      if (!cursor.equals(point))
        setCursorAt(point.x, point.y, skipLocked);
    }
  }

  public Point findFreeCell(Point from, int dx, int dy) {
    Point result = null;
    if (from != null && (dx != 0 || dy != 0)) {
      Point scan = new Point(from);
      while (result == null) {
        scan.x += dx;
        scan.y += dy;
        if (scan.x < 0 || scan.x >= nCols || scan.y < 0 || scan.y >= nRows)
          break;
        if (!getCellAttribute(scan.x, scan.y, LOCKED))
          result = scan;
      }
    }
    return result;
  }

  public boolean isIntoBlacks(Point pt, boolean checkHorizontal) {
    boolean result;
    if (checkHorizontal) {
      result = (pt.x <= 0 || getCellAttribute(pt.x - 1, pt.y, LOCKED))
          && (pt.x >= nCols - 1 || getCellAttribute(pt.x + 1, pt.y, LOCKED));
    } else {
      result = (pt.y <= 0 || getCellAttribute(pt.x, pt.y - 1, LOCKED))
          && (pt.y >= nRows - 1 || getCellAttribute(pt.x, pt.y + 1, LOCKED));
    }
    return result;
  }

  public boolean isIntoWhites(Point pt, boolean checkHorizontal) {
    boolean result;
    if (checkHorizontal) {
      result = (pt.x > 0 && !getCellAttribute(pt.x - 1, pt.y, LOCKED))
          && (pt.x < nCols - 1 && !getCellAttribute(pt.x + 1, pt.y, LOCKED));
    } else {
      result = (pt.y > 0 && !getCellAttribute(pt.x, pt.y - 1, LOCKED))
          && (pt.y < nRows - 1 && !getCellAttribute(pt.x, pt.y + 1, LOCKED));
    }
    return result;
  }

  public Point findNextCellWithAttr(int startX, int startY, int attr, int dx, int dy, boolean attrState) {
    Point point = new Point(startX + dx, startY + dy);
    while (true) {
      if (point.x < 0) {
        point.x = nCols - 1;
        if (point.y > 0)
          point.y--;
        else
          point.y = nRows - 1;
      } else if (point.x >= nCols) {
        point.x = 0;
        if (point.y < nRows - 1)
          point.y++;
        else
          point.y = 0;
      }
      if (point.y < 0) {
        point.y = nRows - 1;
        if (point.x > 0)
          point.x--;
        else
          point.x = nCols - 1;
      } else if (point.y >= nRows) {
        point.y = 0;
        if (point.x < nCols - 1)
          point.x++;
        else
          point.x = 0;
      }
      if ((point.x == startX && point.y == startY) || getCellAttribute(point.x, point.y, attr) == attrState)
        break;
      point.x += dx;
      point.y += dy;
    }
    return point;
  }

  public void setCursorAt(int px, int py, boolean skipLocked) {
    stopCursorBlink();
    if (isValidCell(px, py)) {
      cursor.x = px;
      cursor.y = py;
      useCursor = true;
      if (skipLocked && getCellAttribute(px, py, LOCKED)) {
        moveCursor(1, 0, skipLocked);
      } else {
        if (cursorEnabled)
          startCursorBlink();
      }
    }
  }

  public void setUseCursor(boolean value) {
    useCursor = value;
  }

  public Point getCursor() {
    return cursor;
  }

  public int countCharsLike(char ch) {
    int result = 0;
    for (int py = 0; py < nRows; py++)
      for (int px = 0; px < nCols; px++)
        if (chars[py][px] == ch)
          result++;
    return result;
  }

  public int getNumCells() {
    return nRows * nCols;
  }

  public int countCoincidences(boolean checkCase) {
    int result = 0;
    if (answers != null)
      for (int py = 0; py < nRows; py++)
        for (int px = 0; px < nCols; px++)
          if (isCellOk(px, py, checkCase))
            result++;
    return result;
  }

  public boolean isCellOk(int px, int py, boolean checkCase) {
    boolean result = false;
    if (isValidCell(px, py)) {
      char ch = chars[py][px];
      if (ch != wild) {
        char ch2 = answers[py][px];
        if (ch == ch2 || (!checkCase && Character.toUpperCase(ch) == Character.toUpperCase(ch2)))
          result = true;
      }
    }
    return result;
  }

  public Point getLogicalCoords(Point2D devicePoint) {
    if (!contains(devicePoint))
      return null;
    int px = (int) ((devicePoint.getX() - getX()) / cellWidth);
    int py = (int) ((devicePoint.getY() - getY()) / cellHeight);
    if (isValidCell(px, py)) {
      return new Point(px, py);
    } else
      return null;
  }

  public boolean isValidCell(int px, int py) {
    return px < nCols && py < nRows && px >= 0 && py >= 0;
  }

  public void setCharAt(int px, int py, char ch) {
    if (isValidCell(px, py)) {
      chars[py][px] = ch;
      repaintCell(px, py);
    }
  }

  public char getCharAt(int px, int py) {
    if (isValidCell(px, py))
      return chars[py][px];
    else
      return ' ';
  }

  public String getStringBetween(int x0, int y0, int x1, int y1) {
    StringBuilder sb = new StringBuilder();
    if (isValidCell(x0, y0) && isValidCell(x1, y1)) {
      int dx = x1 - x0;
      int dy = y1 - y0;
      if (dx == 0 || dy == 0 || Math.abs(dx) == Math.abs(dy)) {
        int steps = Math.max(Math.abs(dx), Math.abs(dy));
        if (steps > 0) {
          dx /= steps;
          dy /= steps;
        }
        for (int i = 0; i <= steps; i++)
          sb.append(getCharAt(x0 + dx * i, y0 + dy * i));
      }
    }
    return sb.substring(0);
  }

  public void setAttributeBetween(int x0, int y0, int x1, int y1, int attribute, boolean value) {
    if (isValidCell(x0, y0) && isValidCell(x1, y1)) {
      int dx = x1 - x0;
      int dy = y1 - y0;
      if (dx == 0 || dy == 0 || Math.abs(dx) == Math.abs(dy)) {
        int steps = Math.max(Math.abs(dx), Math.abs(dy));
        if (steps > 0) {
          dx /= steps;
          dy /= steps;
        }
        for (int i = 0; i <= steps; i++)
          setAttribute(x0 + dx * i, y0 + dy * i, attribute, value);
      }
    }
  }

  public void setAttribute(int px, int py, int attribute, boolean state) {
    if (isValidCell(px, py)) {
      if (attribute == MARKED && !state)
        repaintCell(px, py);
      attributes[py][px] &= ~attribute;
      attributes[py][px] |= (state ? attribute : 0);
      if (attribute != MARKED || state)
        repaintCell(px, py);
    }
  }

  public void setAllCellsAttribute(int attribute, boolean state) {
    for (int py = 0; py < nRows; py++)
      for (int px = 0; px < nCols; px++)
        setAttribute(px, py, attribute, state);
  }

  public boolean getCellAttribute(int px, int py, int attribute) {
    if (isValidCell(px, py))
      return (attributes[py][px] & attribute) != 0;
    else
      return false;
  }

  public Rectangle2D getCellRect(int px, int py) {
    return new Double(getX() + px * cellWidth, getY() + py * cellHeight, cellWidth, cellHeight);
  }

  public Rectangle getCellBorderBounds(int px, int py) {
    boolean isMarked = getCellAttribute(px, py, MARKED);
    if (!border && !isMarked)
      return getCellRect(px, py).getBounds();
    BoxBase bb = getBoxBaseResolve();
    Stroke strk = isMarked ? bb.getMarker() : bb.getBorder();
    return strk.createStrokedShape(getCellRect(px, py)).getBounds();
  }

  public void repaintCell(int px, int py) {
    JComponent jc = getContainerResolve();
    if (jc != null)
      jc.repaint(getCellBorderBounds(px, py));
  }

  @Override
  public Object clone() {
    TextGrid tgb = (TextGrid) super.clone();
    tgb.nRows = nRows;
    tgb.nCols = nCols;
    tgb.chars = new char[nRows][nCols];
    tgb.attributes = new int[nRows][nCols];
    for (int i = 0; i < nRows; i++) {
      System.arraycopy(chars[i], 0, tgb.chars[i], 0, nCols);
      System.arraycopy(attributes[i], 0, tgb.attributes[i], 0, nCols);
    }
    tgb.cellWidth = cellWidth;
    tgb.cellHeight = cellHeight;
    tgb.preferredBounds = (Rectangle2D) preferredBounds.clone();
    return tgb;
  }

  public Dimension getPreferredSize() {
    return preferredBounds.getBounds().getSize();
  }

  public Dimension getMinimumSize() {
    return new Dimension(MIN_CELL_SIZE * nCols, MIN_CELL_SIZE * nRows);
  }

  public Dimension getScaledSize(double scale) {
    return new Dimension(StrUtils.roundTo(scale * preferredBounds.getWidth(), nCols),
        StrUtils.roundTo(scale * preferredBounds.getHeight(), nRows));
  }

  @Override
  public void setBounds(Rectangle2D r) {
    super.setBounds(r);
    cellWidth = width / nCols;
    cellHeight = height / nRows;
  }

  @Override
  public boolean update(Graphics2D g2, Rectangle dirtyRegion, ImageObserver io) {
    if (isEmpty() || !isVisible() || isTemporaryHidden())
      return false;
    if (dirtyRegion != null && !shape.intersects(dirtyRegion))
      return false;

    updateContent(g2, dirtyRegion, io);

    return true;
  }

  public boolean updateContent(Graphics2D g2, Rectangle dirtyRegion, ImageObserver io) {

    FontRenderContext frc = g2.getFontRenderContext();
    BoxBase bb = getBoxBaseResolve();

    // test font size
    FontMetrics fm = g2.getFontMetrics(bb.getFont());
    boolean resize = false;
    while (true) {
      if (fm.charWidth('W') <= cellWidth - 2 * MIN_INTERNAL_MARGIN
          && fm.getAscent() + fm.getDescent() <= cellHeight - 2 * MIN_INTERNAL_MARGIN)
        break;
      if (bb.reduceFont() == false)
        break;
      resize = true;
      fm = g2.getFontMetrics(bb.getFont());
    }
    if (resize) {
      JComponent jc = getContainerResolve();
      if (jc != null)
        RepaintManager.currentManager(jc).markCompletelyDirty(jc);
      return true;
    }

    char[] ch = new char[1];
    int attr;
    boolean isMarked, isInverted, isCursor;
    Rectangle2D boxBounds;
    double dx, dy;
    double ry = (cellHeight - fm.getDescent() + fm.getAscent()) / 2;

    for (int py = 0; py < nRows; py++) {
      for (int px = 0; px < nCols; px++) {
        Rectangle bxr = getCellBorderBounds(px, py);
        if (bxr.intersects(dirtyRegion)) {
          attr = attributes[py][px];
          if ((attr & TRANSPARENT) == 0) {
            isInverted = (attr & INVERTED) != 0;
            isMarked = (attr & MARKED) != 0;
            isCursor = (useCursor && cursor.x == px && cursor.y == py);
            boxBounds = getCellRect(px, py);
            g2.setColor((isCursor && cursorBlink) ? bb.inactiveColor : isInverted ? bb.textColor : bb.backColor);
            g2.fill(boxBounds);
            g2.setColor(Color.black);
            if ((attr & HIDDEN) == 0) {
              ch[0] = chars[py][px];
              if (ch[0] != 0) {
                dx = boxBounds.getX() + (cellWidth - fm.charWidth(ch[0])) / 2;
                dy = boxBounds.getY() + ry;
                GlyphVector gv = bb.getFont().createGlyphVector(frc, ch);
                if (bb.shadow) {
                  g2.setColor(bb.shadowColor);
                  g2.drawGlyphVector(gv, (float) (dx + bb.getDynFontSize() / 10),
                      (float) (dy + bb.getDynFontSize() / 10));
                }
                g2.setColor(isInverted ? bb.backColor : isAlternative() ? bb.alternativeColor : bb.textColor);
                g2.drawGlyphVector(gv, (float) dx, (float) dy);
              }
            }
            if (border || isMarked) {
              g2.setColor(bb.borderColor);
              g2.setStroke(isMarked ? bb.getMarker() : bb.getBorder());
              if (isMarked)
                g2.setXORMode(Color.white);
              g2.draw(boxBounds);
              if (isMarked)
                g2.setPaintMode();
              g2.setStroke(BoxBase.DEFAULT_STROKE);
            }
            g2.setColor(Color.black);
          }
        }
      }
    }
    return true;
  }

  public void actionPerformed(ActionEvent ev) {
    blink(0);
  }

  protected synchronized void blink(int status) {
    if (useCursor) {
      cursorBlink = status == 1 ? true : status == -1 ? false : !cursorBlink;
      repaintCell(cursor.x, cursor.y);
    }
  }

  @Override
  public void end() {
    if (cursorTimer != null) {
      cursorTimer.stop();
      cursorTimer = null;
    }
  }

  public void finalize() throws Throwable {
    try {
      end();
    } finally {
      super.finalize();
    }
  }
}
