/*
Copyright 2005-2013 Samuel Gesche

This file is part of ArcEnCiel.

ArcEnCiel 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 3 of the License, or
(at your option) any later version.

ArcEnCiel 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.

You should have received a copy of the GNU General Public License
along with ArcEnCiel.  If not, see <http://www.gnu.org/licenses/>.
*/

package ihm.townto;

import ihm.Charte;

import javax.swing.JTextPane;
import javax.swing.JButton;
import javax.swing.ImageIcon;
import javax.swing.text.Style;
import javax.swing.text.StyleContext;
import javax.swing.text.StyleConstants;
import javax.swing.text.StyledDocument;
import javax.swing.text.Utilities;
import javax.swing.text.BadLocationException;
import java.awt.Image;
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;
import java.net.URL;

//-- Redefinition of some classes from the Java Standard API --//
//-- Taken from internet, at URL --//
//-- forum.java.sun.com/thread.jsp?forum=57&thread=289879 --//

import javax.swing.SizeRequirements;
import javax.swing.text.StyledEditorKit;
import javax.swing.text.ViewFactory;
import javax.swing.text.Element;
import javax.swing.text.View;
import javax.swing.text.ParagraphView;
import javax.swing.text.FlowView;
import javax.swing.text.BoxView;
import javax.swing.text.AttributeSet;
import javax.swing.text.LabelView;
import javax.swing.text.IconView;
import javax.swing.text.ComponentView;
import javax.swing.text.AbstractDocument;
import javax.swing.text.Position;
import java.awt.Shape;
import java.awt.Rectangle;


/**
 * This class contains the styled text displayed in the frame InfoFrame.
 * <br>
 * There are five defined text styles, each of them identified with a String.
 * Multimedia resources can be added to the text as well. Images are displayed
 * on the component, whereas sounds are displayed as buttons (the sound is
 * played on click).
 * <br>
 * Since this component is here to display a text, it is not editable.
 *
 * @author Samuel Gesche
 * @version 1.4
 * @since Towntology 1.0
 */

public class StyledTextPanel
    extends JTextPane{
  private int maxResources = 0;

  /**
   * The style for default text.
   * @since Towntology 1.0
   */
  public final static String STYLE_TEXT = "text";
  /**
   * The style for default text.
   * @since Towntology 1.0
   */
  protected Style text;

  /**
   * The style for titles.
   * @since Towntology 1.0
   */
  public final static String STYLE_TITLE = "title";
  /**
   * The style for titles.
   * @since Towntology 1.0
   */
  protected Style title;

  /**
   * The style for domain names.
   * @since Towntology 1.0
   */
  public final static String STYLE_DOMAIN = "domain";
  /**
   * The style for domain names.
   * @since Towntology 1.0
   */
  protected Style domain;

  /**
   * The style for sources and references.
   * @since Towntology 1.0
   */
  public final static String STYLE_SOURCE = "source";
  /**
   * The style for sources and references.
   * @since Towntology 1.0
   */
  protected Style source;

  /**
   * The style for multimedia resources definitions.
   * @since Towntology 1.0
   */
  public final static String STYLE_TEXT2 = "text2";
  /**
   * The style for multimedia resources definitions.
   * @since Towntology 1.0
   */
  protected Style text2;

  /**
   * The style for resource description.
   * @since Towntology 1.0
   */
  public final static String STYLE_LEGEND = "legend";
  /**
   * The style for resource description.
   * @since Towntology 1.0
   */
  protected Style legend;

  /**
   * The style for graph nodes.
   * @since Towntology 1.0
   */
  public final static String STYLE_GRAPH = "graph";
  /**
   * The style for graph nodes.
   * @since Towntology 1.0
   */
  protected Style graph;

  private StyledDocument doc;

  /**
   * Creates a new StyledTextPanel.
   * @since Towntology 1.0
   */
  public StyledTextPanel(){
    //setBackground(ColorTable.COLOR_FRAME_BACKGROUND);
    setEditable(false);
    setDoubleBuffered(true);
    setEditorKit(new FixedStyledEditorKit());
    setOpaque(false);

    doc = (StyledDocument)getDocument(); //getStyledDocument();

    Style def = StyleContext.getDefaultStyleContext().
                getStyle(StyleContext.DEFAULT_STYLE);
    StyleConstants.setForeground(def, getForeground());
    StyleConstants.setAlignment(def, StyleConstants.ALIGN_JUSTIFIED);

    text = doc.addStyle(STYLE_TEXT, def);
    StyleConstants.setFontFamily(def, Charte.getFont().getFamily());
    //StyleConstants.setForeground(text, ColorTable.COLOR_TEXT_MAIN);
    StyleConstants.setFontSize(text, 14);
    StyleConstants.setAlignment(text, StyleConstants.ALIGN_JUSTIFIED);

    title = doc.addStyle(STYLE_TITLE, text);
    //StyleConstants.setForeground(title, ColorTable.COLOR_TEXT_TITLE);
    StyleConstants.setUnderline(title, true);
    StyleConstants.setAlignment(title, StyleConstants.ALIGN_CENTER);

    domain = doc.addStyle(STYLE_DOMAIN, text);
    //StyleConstants.setForeground(domain, ColorTable.COLOR_TEXT_MAIN2);
    StyleConstants.setItalic(domain, true);

    source = doc.addStyle(STYLE_SOURCE, domain);
    //StyleConstants.setForeground(source, ColorTable.COLOR_TEXT_SOURCE);
    StyleConstants.setFontSize(source, 12);
    StyleConstants.setAlignment(source, StyleConstants.ALIGN_RIGHT);

    text2 = doc.addStyle(STYLE_TEXT2, text);
    //StyleConstants.setForeground(text2, ColorTable.COLOR_TEXT_DARKER);

    legend = doc.addStyle(STYLE_LEGEND, text2);
    StyleConstants.setAlignment(legend, StyleConstants.ALIGN_CENTER);

    graph = doc.addStyle(STYLE_GRAPH, text);
    //StyleConstants.setForeground(graph, ColorTable.COLOR_CELL_TEXT);
    StyleConstants.setAlignment(graph, StyleConstants.ALIGN_CENTER);
    StyleConstants.setFontSize(graph, 10);
  }

  /**
   * Returns the number of lines.
   * @return the number of lines.
   * since Towntology 1.3
   */
  public int getLineCount(){
    int rowCount = 0;
    int len = doc.getLength();
    int offset = 0;
    try{
      while (offset < len){
        int end = Utilities.getRowEnd(this, offset);
        if (end < 0){
          break;
        }
        rowCount++;
        // Include the last character on the line
        end = Math.min(end + 1, len);
        offset = end;
      }
    }
    catch (BadLocationException e){
    }
    catch(ArrayIndexOutOfBoundsException aiobe){
    }
    return rowCount==0?2:rowCount;
  }

  /**
   * Clears all text.
   * @since Towntology 1.2
   */
  public void clear(){
    try{
      doc.remove(0, doc.getLength());
    }
    catch (BadLocationException ble){
      ble.printStackTrace();
    }
  }

  /**
   * Writes the given text with the given style at the end of the document.
   * @param text the text to write.
   * @param style the style to use (one of the constant strings).
   * @since Towntology 1.0
   */
  public void write(String text, String style){
    if (text == null){
      return;
    }
    try{
      int o = doc.getLength();
      int l = text.length();
      doc.insertString(doc.getLength(), text, doc.getStyle(style));
      doc.setParagraphAttributes(o, l, doc.getStyle(style), true);
    }
    catch (BadLocationException ble){
      ble.printStackTrace();
    }
  }

  /**
   * Adds the specified image at the end of the document.
   * <br>
   * The supported images are gif, jpeg and png files.
   * @param image the image to add.
   * @param description the description of the image.
   * @since Towntology 1.0
   */
  public void addImage(Image image, String description){
    maxResources++;
    Style s = doc.addStyle("" + maxResources, text);
    StyleConstants.setAlignment(s, StyleConstants.ALIGN_CENTER);
    StyleConstants.setIcon(s, new ImageIcon(image));
    write("\n", "" + maxResources);
    write("" + description, STYLE_LEGEND);
  }

  /**
   * Adds the specified sound (in the form of a button) at the end of the
   * document.
   * <br>
   * The supported sounds are wave and au files.
   * @param soundURL the URL containing the sound.
   * @param description the description of the sound.
   * @since Towntology 1.0
   */
  public void addSound(URL soundURL, String description){
    maxResources++;
    Style s = doc.addStyle("" + maxResources, text);
    ImageIcon icon = new ImageIcon("sound.png");
    JButton button = new JButton(icon);
    final URL soundURL0 = soundURL;
    button.addActionListener(new ActionListener(){
      public void actionPerformed(ActionEvent ae){
        //SoundHandler.playSound(soundURL0);
      }
    });
    StyleConstants.setAlignment(s, StyleConstants.ALIGN_CENTER);
    StyleConstants.setComponent(s, button);
    write("\n", "" + maxResources);
    write("" + description, STYLE_LEGEND);
  }

  /**
   * Adds the specified music (in the form of a button) at the end of the
   * document.
   * <br>
   * The supported musics are midi files.
   * @param musicURL the URL containing the music.
   * @param description the description of the music.
   * @since Towntology 1.0
   */
  public void addMusic(URL musicURL, String description){
    maxResources++;
    Style s = doc.addStyle("" + maxResources, text);
    ImageIcon icon = new ImageIcon("music.png");
    JButton button = new JButton(icon);
    final URL musicURL0 = musicURL;
    button.addActionListener(new ActionListener(){
      public void actionPerformed(ActionEvent ae){
        //SoundHandler.playMusic(musicURL0);
      }
    });
    StyleConstants.setAlignment(s, StyleConstants.ALIGN_CENTER);
    StyleConstants.setComponent(s, button);
    write("\n", "" + maxResources);
    write("" + description, STYLE_LEGEND);
  }

}

//-- Redefinition of some classes from the Java Standard API --//
//-- Taken from internet, at URL --//
//-- forum.java.sun.com/thread.jsp?forum=57&thread=289879 --//

class FixedStyledEditorKit
    extends StyledEditorKit{
  public ViewFactory getViewFactory(){
    return new FixedStyledViewFactory();
  }
}

class FixedStyledViewFactory
    implements ViewFactory{
  public View create(Element elem){
    String kind = elem.getName();
    if (kind != null){
      if (kind.equals(AbstractDocument.ContentElementName)){
        return new LabelView(elem);
      }
      else if (kind.equals(AbstractDocument.ParagraphElementName)){
        // return new ParagraphView(elem);
        return new FixedParagraphView(elem);
      }
      else if (kind.equals(AbstractDocument.SectionElementName)){
        return new BoxView(elem, View.Y_AXIS);
      }
      else if (kind.equals(StyleConstants.ComponentElementName)){
        return new ComponentView(elem);
      }
      else if (kind.equals(StyleConstants.IconElementName)){
        return new IconView(elem);
      }
    }
    // default to text display
    return new LabelView(elem);
  }
}

class FixedParagraphView
    extends ParagraphView{
  public FixedParagraphView(Element elem){
    super(elem);
    strategy = new FixedFlowStrategy();
  }

  protected View createRow(){
    Element elem = getElement();
    return new FixedRow(elem);
  }

  protected static int getSpaceCount(String content){
    int result = 0;
    int index = content.indexOf(' ');
    while (index >= 0){
      result++;
      index = content.indexOf(' ', index + 1);
    }
    return result;
  }

  protected static int[] getSpaceIndexes(String content, int shift){
    int cnt = getSpaceCount(content);
    int[] result = new int[cnt];
    int counter = 0;
    int index = content.indexOf(' ');
    while (index >= 0){
      result[counter] = index + shift;
      counter++;
      index = content.indexOf(' ', index + 1);
    }
    return result;
  }

  static class FixedFlowStrategy
      extends FlowStrategy{
    public void layout(FlowView fv){
      super.layout(fv);
      AttributeSet attr = fv.getElement().getAttributes();
      float lineSpacing = StyleConstants.getLineSpacing(attr);
      boolean justifiedAlignment = (StyleConstants.getAlignment(attr) ==
                                    StyleConstants.ALIGN_JUSTIFIED);
      if (!(justifiedAlignment || (lineSpacing > 1))){
        return;
      }
      int cnt = fv.getViewCount();
      for (int i = 0; i < cnt - 1; i++){
        FixedRow row = (FixedRow)fv.getView(i);
        if (lineSpacing > 1){
          float height = row.getMinimumSpan(View.Y_AXIS);
          float addition = (height * lineSpacing) - height;
          if (addition > 0){
            row.setInsets(row.getTopInset(), row.getLeftInset(),
                          (short)addition, row.getRightInset());
          }
        }
        if (justifiedAlignment){
          restructureRow(row, i);
          row.setRowNumber(i + 1);
        }
      }
    }

    protected void restructureRow(View row, int rowNum){
      int rowStartOffset = row.getStartOffset();
      int rowEndOffset = row.getEndOffset();
      String rowContent = "";
      try{
        rowContent = row.getDocument().getText(rowStartOffset,
                                               rowEndOffset - rowStartOffset);
        if (rowNum == 0){
          int index = 0;
          while (rowContent.charAt(0) == ' '){
            rowContent = rowContent.substring(1);
            if (rowContent.length() == 0){
              break;
            }
          }
        }
      }
      catch (Exception e){
        e.printStackTrace();
      }
      int rowSpaceCount = getSpaceCount(rowContent);
      if (rowSpaceCount < 1){
        return;
      }
      int[] rowSpaceIndexes = getSpaceIndexes(rowContent, row.getStartOffset());
      int currentSpaceIndex = 0;
      for (int i = 0; i < row.getViewCount(); i++){
        View child = row.getView(i);
        if ((child.getStartOffset() < rowSpaceIndexes[currentSpaceIndex]) &&
            (child.getEndOffset() > rowSpaceIndexes[currentSpaceIndex])){
          //split view
          View first = child.createFragment(child.getStartOffset(),
                                            rowSpaceIndexes[currentSpaceIndex]);
          View second = child.createFragment(rowSpaceIndexes[currentSpaceIndex],
                                             child.getEndOffset());
          View[] repl = new View[2];
          repl[0] = first;
          repl[1] = second;
          row.replace(i, 1, repl);
          currentSpaceIndex++;
          if (currentSpaceIndex >= rowSpaceIndexes.length){
            break;
          }
        }
      }
      int childCnt = row.getViewCount();
    }
  }

  class FixedRow
      extends BoxView{
    private int rowNumber = 0;
    FixedRow(Element elem){
      super(elem, View.X_AXIS);
    }

    protected void loadChildren(ViewFactory f){
    }

    public AttributeSet getAttributes(){
      View p = getParent();
      return (p != null) ? p.getAttributes() : null;
    }

    public float getAlignment(int axis){
      if (axis == View.X_AXIS){
        AttributeSet attr = getAttributes();
        int justification = StyleConstants.getAlignment(attr);
        switch (justification){
          case StyleConstants.ALIGN_LEFT:
          case StyleConstants.ALIGN_JUSTIFIED:
            return 0;
          case StyleConstants.ALIGN_RIGHT:
            return 1;
          case StyleConstants.ALIGN_CENTER:
            return 0.5f;
        }
      }
      return super.getAlignment(axis);
    }

    public Shape modelToView(int pos, Shape a, Position.Bias b) throws
        BadLocationException{
      Rectangle r = a.getBounds();
      View v = getViewAtPosition(pos, r);
      if ((v != null) && (!v.getElement().isLeaf())){
        // Don't adjust the height if the view represents a branch.
        return super.modelToView(pos, a, b);
      }
      r = a.getBounds();
      int height = r.height;
      int y = r.y;
      Shape loc = super.modelToView(pos, a, b);
      r = loc.getBounds();
      r.height = height;
      r.y = y;
      return r;
    }

    public int getStartOffset(){
      int offs = Integer.MAX_VALUE;
      int n = getViewCount();
      for (int i = 0; i < n; i++){
        View v = getView(i);
        offs = Math.min(offs, v.getStartOffset());
      }
      return offs;
    }

    public int getEndOffset(){
      int offs = 0;
      int n = getViewCount();
      for (int i = 0; i < n; i++){
        View v = getView(i);
        offs = Math.max(offs, v.getEndOffset());
      }
      return offs;
    }

    protected void layoutMinorAxis(int targetSpan, int axis, int[] offsets,
                                   int[] spans){
      baselineLayout(targetSpan, axis, offsets, spans);
    }

    protected SizeRequirements calculateMinorAxisRequirements(int axis,
        SizeRequirements r){
      return baselineRequirements(axis, r);
    }

    protected int getViewIndexAtPosition(int pos){
      // This is expensive, but are views are not necessarily layed
      // out in model order.
      if (pos < getStartOffset() || pos >= getEndOffset()){
        return -1;
      }
      for (int counter = getViewCount() - 1; counter >= 0; counter--){
        View v = getView(counter);
        if (pos >= v.getStartOffset() &&
            pos < v.getEndOffset()){
          return counter;
        }
      }
      return -1;
    }

    public short getTopInset(){
      return super.getTopInset();
    }

    public short getLeftInset(){
      return super.getLeftInset();
    }

    public short getRightInset(){
      return super.getRightInset();
    }

    public void setInsets(short topInset, short leftInset, short bottomInset,
                          short rightInset){
      super.setInsets(topInset, leftInset, bottomInset, rightInset);
    }

    protected void layoutMajorAxis(int targetSpan, int axis, int[] offsets,
                                   int[] spans){
      super.layoutMajorAxis(targetSpan, axis, offsets, spans);
      AttributeSet attr = getAttributes();
      if ((StyleConstants.getAlignment(attr) != StyleConstants.ALIGN_JUSTIFIED) &&
          (axis != View.X_AXIS)){
        return;
      }
      int cnt = offsets.length;
      int span = 0;
      for (int i = 0; i < cnt; i++){
        span += spans[i];
      }
      if (getRowNumber() == 0){
        return;
      }
      int startOffset = getStartOffset();
      int len = getEndOffset() - startOffset;
      String context = "";
      try{
        context = getElement().getDocument().getText(startOffset, len);
      }
      catch (Exception e){
        e.printStackTrace();
      }
      int spaceCount = getSpaceCount(context) - 1;
      int pixelsToAdd = targetSpan - span;
      if (this.getRowNumber() == 1){
        int firstLineIndent = (int)StyleConstants.getFirstLineIndent(
            getAttributes());
        pixelsToAdd -= firstLineIndent;
      }
      int[] spaces = getSpaces(pixelsToAdd, spaceCount);
      int j = 0;
      int shift = 0;
      for (int i = 1; i < cnt; i++){
        LabelView v = (LabelView)getView(i);
        offsets[i] += shift;
        if ((isContainSpace(v)) && (i != cnt - 1)){
          offsets[i] += spaces[j];
          spans[i - 1] += spaces[j];
          shift += spaces[j];
          j++;
        }
      }
    }

    protected int[] getSpaces(int space, int cnt){
      int[] result = new int[cnt];
      if (cnt == 0){
        return result;
      }
      int base = space / cnt;
      int rst = space % cnt;
      for (int i = 0; i < cnt; i++){
        result[i] = base;
        if (rst > 0){
          result[i]++;
          rst--;
        }
      }
      return result;
    }

    public float getMinimumSpan(int axis){
      if (axis == View.X_AXIS){
        AttributeSet attr = getAttributes();
        if (StyleConstants.getAlignment(attr) != StyleConstants.ALIGN_JUSTIFIED){
          return super.getMinimumSpan(axis);
        }
        else{
          return this.getParent().getMinimumSpan(axis);
        }
      }
      else{
        return super.getMinimumSpan(axis);
      }
    }

    public float getMaximumSpan(int axis){
      if (axis == View.X_AXIS){
        AttributeSet attr = getAttributes();
        if (StyleConstants.getAlignment(attr) != StyleConstants.ALIGN_JUSTIFIED){
          return super.getMaximumSpan(axis);
        }
        else{
          return this.getParent().getMaximumSpan(axis);
        }
      }
      else{
        return super.getMaximumSpan(axis);
      }
    }

    public float getPreferredSpan(int axis){
      if (axis == View.X_AXIS){
        AttributeSet attr = getAttributes();
        if (StyleConstants.getAlignment(attr) != StyleConstants.ALIGN_JUSTIFIED){
          return super.getPreferredSpan(axis);
        }
        else{
          return this.getParent().getPreferredSpan(axis);
        }
      }
      else{
        return super.getPreferredSpan(axis);
      }
    }

    public void setRowNumber(int value){
      rowNumber = value;
    }

    public int getRowNumber(){
      return rowNumber;
    }
  }

  public int getFlowSpan(int index){
    int span = super.getFlowSpan(index);
    if (index == 0){
      int firstLineIdent = (int)StyleConstants.getFirstLineIndent(this.
          getAttributes());
      span -= firstLineIdent;
    }
    return span;
  }

  protected void layoutMinorAxis(int targetSpan, int axis, int[] offsets,
                                 int[] spans){
    super.layoutMinorAxis(targetSpan, axis, offsets, spans);
    int firstLineIdent = (int)StyleConstants.getFirstLineIndent(this.
        getAttributes());
    offsets[0] += firstLineIdent;
  }

  protected static boolean isContainSpace(View v){
    int startOffset = v.getStartOffset();
    int len = v.getEndOffset() - startOffset;
    try{
      String text = v.getDocument().getText(startOffset, len);
      if (text.indexOf(' ') >= 0){
        return true;
      }
      else{
        return false;
      }
    }
    catch (Exception ex){
      return false;
    }
  }

}
