/**
    Copyright 2004-2008 Ricard Marxer  <email@ricardmarxer.com>

    This file is part of Geomerative.

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

    Geomerative 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 Geomerative.  If not, see <http://www.gnu.org/licenses/>.
*/

package geomerative;
import processing.core.*;

import org.apache.batik.svggen.font.*;
import org.apache.batik.svggen.font.table.*;

/**
 * RShape is a reduced interface for creating, holding and drawing text from TrueType Font files. It's a basic interpreter of TrueType fonts enabling to access any String in the form of a group of shapes.  Enabling us in this way to access their geometry.
 * @eexample RFont
 * @usage Geometry
 * @related RGroup
 *
 * @extended
 */
public class RFont implements PConstants{
  Font f;
  float scaleFactor = 0.2F;
  //int scaleFactorFixed = 1;
  
  /**
   * The point size of the font. 
   * @eexample size
   * @related setSize ( )
   * @related RFont
   */
  public int size = DEFAULT_SIZE;
  
  /**
   * The alignment of the font. This property can take the following values: RFont.LEFT, RFont.CENTER and RFont.RIGHT
   * @eexample align
   * @related setAlign ( )
   * @related RFont
   */
  public int align = DEFAULT_ALIGN;
    
  final static int DEFAULT_SIZE = 48;
  final static int DEFAULT_RESOLUTION = 72;
  final static int DEFAULT_ALIGN = RFont.LEFT;
  
  /**
   * Should we try to use ASCII, rather than Unicode?
   */
  public boolean forceAscii = false;
  
  
  /**
   * The constructor of the RFont object.  Use this in order to create a font with which we will be able to draw and obtain outlines of text.
   * @eexample RFont
   * @param fontPath String, the name of the TrueType Font file which should be situated in the data folder of the sketch.
   * @param size int, the point size of the font in points.
   * @param align int, this can only take the following values: RFont.LEFT, RFont.CENTER and RFont.RIGHT.
   * @related toGroup ( )
   * @related toShape ( )
   * @related toPolygon ( )
   * @related toMesh ( )
   * @related draw ( )
   */
  public RFont(String fontPath, int size, int align) throws RuntimeException{   
    // Try to find the font as font path
    byte[] bs = RG.parent().loadBytes(fontPath);
    f = Font.create(bs);
    
    setSize(size);
    setAlign(align);
  }
  
  public RFont(String fontPath, int size) throws RuntimeException{
    this(fontPath, size, DEFAULT_ALIGN);
  }
  
  public RFont(String fontPath) throws RuntimeException{
    this(fontPath, DEFAULT_SIZE, DEFAULT_ALIGN);
  }
  
  /**
   * Use this method to reset the point size of the font.
   * @eexample setSize
   * @param size int, the point size of the font in points.
   * @related size
   * @related RFont
   */
  public void setSize(int size){
    short unitsPerEm = f.getHeadTable().getUnitsPerEm();
    int resolution = RG.dpi();
    this.scaleFactor = ((float)size * (float)resolution) / (72F * (float)unitsPerEm);
    //this.scaleFactorFixed = (int)(this.scaleFactor * 65536F);
    //System.out.println(scaleFactor);
    //System.out.println(scaleFactorFixed);
  }

  public float getLineSpacing() {
    // More info at:
    //    http://fontforge.sourceforge.net/faq.html#linespace
    //    http://typophile.com/node/13081
    short unitsPerEm = f.getHeadTable().getUnitsPerEm();
    System.out.println("UnitsPerEm (emsize): " + unitsPerEm);

    // HHEA table method:
    float hheaLineGap = (f.getHheaTable().getAscender() - f.getHheaTable().getDescender() + f.getHheaTable().getLineGap()) * this.scaleFactor;
    System.out.println("HHEA lineGap: " + hheaLineGap);

    // OS2 table typographic line gap method:
    float os2TypoLineGap = (f.getOS2Table().getTypoAscender() - f.getOS2Table().getTypoDescender() + f.getOS2Table().getTypoLineGap()) * this.scaleFactor;
    System.out.println("Os2 Typo lineGap: " + os2TypoLineGap);

    // OS2 table win line gap method:
    float os2WinLineGap = (f.getOS2Table().getWinAscent() + f.getOS2Table().getWinDescent()) * this.scaleFactor;
    System.out.println("Os2 Win lineGap: " + os2WinLineGap);

    // Automatic calculation
    float autoLineGap = f.getHeadTable().getUnitsPerEm() * 1.25f * this.scaleFactor;
    System.out.println("Automatic lineGap: " + autoLineGap);

    return hheaLineGap;
  }
  
  /**
   * Use this method to reset the alignment of the font. This property can take the following values: RFont.LEFT, RFont.CENTER and RFont.RIGHT
   * @eexample setAlign
   * @param align int, this can only take the following values: RFont.LEFT, RFont.CENTER and RFont.RIGHT.
   * @related align
   * @related RFont
   */
  public void setAlign(int align) throws RuntimeException{
    if(align!=LEFT && align!=CENTER && align!=RIGHT){
      throw new RuntimeException("Alignment unknown.  The only accepted values are: RFont.LEFT, RFont.CENTER and RFont.RIGHT");
    }
    this.align = align;
  }
  
  /**
   * @invisible
   **/
  public String getFamily(){
    return f.getNameTable().getRecord(org.apache.batik.svggen.font.table.Table.nameFontFamilyName);
  }
  
  /**
   * Use this method to get the outlines of a character in the form of an RShape.
   * @eexample RFont_toShape
   * @param character char, the character we want the outline from.
   * @return RShape, the outline of the character.
   * @related toGroup ( )
   * @related toPolygon ( )
   * @related draw ( )
   */
  public RShape toShape(char character){
    RGroup grp = toGroup(Character.toString(character));
    if(grp.countElements()>0) return (RShape)(grp.elements[0]);
    return new RShape();
  }
  
  /**
   * Use this method to get the outlines of a character in the form of an RPolygon.
   * @eexample RFont_toPolygon
   * @param character char, the character we want the outline from.
   * @return RPolygon, the outline of the character.
   * @related toGroup ( )
   * @related toShape ( )
   * @related draw ( )
   */
  public RPolygon toPolygon(char character) {
    return toShape(character).toPolygon();
  }
  
  
  private CmapFormat getCmapFormat() {
    if (forceAscii) {
      // We've been asked to use the ASCII/Macintosh cmap format
      return f.getCmapTable().getCmapFormat(
                                            org.apache.batik.svggen.font.table.Table.platformMacintosh,
                                            org.apache.batik.svggen.font.table.Table.encodingRoman );
    } else {
      short[] platforms = new short[] {
        org.apache.batik.svggen.font.table.Table.platformMicrosoft,
        org.apache.batik.svggen.font.table.Table.platformAppleUnicode,
        org.apache.batik.svggen.font.table.Table.platformMacintosh
      };
      short[] encodings = new short[] {
        org.apache.batik.svggen.font.table.Table.encodingUGL,
        org.apache.batik.svggen.font.table.Table.encodingKorean,
        org.apache.batik.svggen.font.table.Table.encodingHebrew,
        org.apache.batik.svggen.font.table.Table.encodingUndefined
      };
      
      CmapFormat cmapFmt;
      for(int i = 0; i < encodings.length; i++) {
        for(int j = 0; j < platforms.length; j++) {
          
          cmapFmt = f.getCmapTable().getCmapFormat(platforms[j], encodings[i]);
          if (cmapFmt != null) {
            return cmapFmt;
          }                    
                }
      }
      return null;
    }
  }

  
  /**
   * Use this method to get the outlines of a string in the form of an RGroup.  All the elements of the group will be RShapes.
   * @eexample RFont_toGroup
   * @param text String, the string we want the outlines from.
   * @return RGroup, the group of outlines of the character.  All the elements are RShapes.
   * @related toShape ( )
   * @related draw ( )
   */
  public RGroup toGroup(String text)  throws RuntimeException{
    RGroup result = new RGroup();
    
    // Decide upon a cmap table to use for our character to glyph look-up
    CmapFormat cmapFmt = getCmapFormat();
    
    if (cmapFmt == null) {
      throw new RuntimeException("Cannot find a suitable cmap table");
    }
    
    // If this font includes arabic script, we want to specify
    // substitutions for initial, medial, terminal & isolated
    // cases.
    /*
      GsubTable gsub = (GsubTable) f.getTable(Table.GSUB);
      SingleSubst initialSubst = null;
      SingleSubst medialSubst = null;
      SingleSubst terminalSubst = null;
      if (gsub != null) {
      Script s = gsub.getScriptList().findScript(ScriptTags.SCRIPT_TAG_ARAB);
      if (s != null) {
      LangSys ls = s.getDefaultLangSys();
      if (ls != null) {
      Feature init = gsub.getFeatureList().findFeature(ls, FeatureTags.FEATURE_TAG_INIT);
      Feature medi = gsub.getFeatureList().findFeature(ls, FeatureTags.FEATURE_TAG_MEDI);
      Feature fina = gsub.getFeatureList().findFeature(ls, FeatureTags.FEATURE_TAG_FINA);
      
      initialSubst = (SingleSubst)
      gsub.getLookupList().getLookup(init, 0).getSubtable(0);
      medialSubst = (SingleSubst)
      gsub.getLookupList().getLookup(medi, 0).getSubtable(0);
      terminalSubst = (SingleSubst)
      gsub.getLookupList().getLookup(fina, 0).getSubtable(0);
      }
      }
      }*/
    
    int x = 0;
    for (short i = 0; i < text.length(); i++) {
      int glyphIndex = cmapFmt.mapCharCode(text.charAt(i));
      Glyph glyph = f.getGlyph(glyphIndex);
      int default_advance_x = f.getHmtxTable().getAdvanceWidth(glyphIndex);
      if (glyph != null) {
        glyph.scale(scaleFactor);
        // Add the Glyph to the Shape with an horizontal offset of x
        result.addElement(getGlyphAsShape(f,glyph, glyphIndex,x));
        x += glyph.getAdvanceWidth();
      }else{
        x += (int)((float)default_advance_x*scaleFactor);
      }
      
    }
    
    if(align!=LEFT && align!=CENTER && align!=RIGHT){
      throw new RuntimeException("Alignment unknown.  The only accepted values are: RFont.LEFT, RFont.CENTER and RFont.RIGHT");
    }
    
    RRectangle r;
    RMatrix mattrans;

    switch(this.align){
    case RFont.CENTER:
      r = result.getBounds();
      mattrans = new RMatrix();
      mattrans.translate((r.getMinX()-r.getMaxX())/2,0);
      result.transform(mattrans);
      break;
    case RFont.RIGHT:
      r = result.getBounds();
      mattrans = new RMatrix();
      mattrans.translate((r.getMinX()-r.getMaxX()),0);
      result.transform(mattrans);
      break;
    case RFont.LEFT:
      break;
    }
    return result;
  }

  public RShape toShape(String text)  throws RuntimeException{
    RShape result = new RShape();
    
    // Decide upon a cmap table to use for our character to glyph look-up
    CmapFormat cmapFmt = getCmapFormat();
    
    if (cmapFmt == null) {
      throw new RuntimeException("Cannot find a suitable cmap table");
    }
    
    // If this font includes arabic script, we want to specify
    // substitutions for initial, medial, terminal & isolated
    // cases.
    /*
      GsubTable gsub = (GsubTable) f.getTable(Table.GSUB);
      SingleSubst initialSubst = null;
      SingleSubst medialSubst = null;
      SingleSubst terminalSubst = null;
      if (gsub != null) {
      Script s = gsub.getScriptList().findScript(ScriptTags.SCRIPT_TAG_ARAB);
      if (s != null) {
      LangSys ls = s.getDefaultLangSys();
      if (ls != null) {
      Feature init = gsub.getFeatureList().findFeature(ls, FeatureTags.FEATURE_TAG_INIT);
      Feature medi = gsub.getFeatureList().findFeature(ls, FeatureTags.FEATURE_TAG_MEDI);
      Feature fina = gsub.getFeatureList().findFeature(ls, FeatureTags.FEATURE_TAG_FINA);
      
      initialSubst = (SingleSubst)
      gsub.getLookupList().getLookup(init, 0).getSubtable(0);
      medialSubst = (SingleSubst)
      gsub.getLookupList().getLookup(medi, 0).getSubtable(0);
      terminalSubst = (SingleSubst)
      gsub.getLookupList().getLookup(fina, 0).getSubtable(0);
      }
      }
      }*/
    
    int x = 0;
    for (short i = 0; i < text.length(); i++) {
      int glyphIndex = cmapFmt.mapCharCode(text.charAt(i));
      Glyph glyph = f.getGlyph(glyphIndex);
      int default_advance_x = f.getHmtxTable().getAdvanceWidth(glyphIndex);
      if (glyph != null) {
        glyph.scale(scaleFactor);
        // Add the Glyph to the Shape with an horizontal offset of x
        result.addChild(getGlyphAsShape(f,glyph, glyphIndex,x));
        x += glyph.getAdvanceWidth();
      }else{
        x += (int)((float)default_advance_x*scaleFactor);
      }
      
    }
    
    if(align!=LEFT && align!=CENTER && align!=RIGHT){
      throw new RuntimeException("Alignment unknown.  The only accepted values are: RFont.LEFT, RFont.CENTER and RFont.RIGHT");
    }
    
    RRectangle r;
    RMatrix mattrans;

    switch(this.align){
    case RFont.CENTER:
      r = result.getBounds();
      mattrans = new RMatrix();
      mattrans.translate((r.getMinX()-r.getMaxX())/2,0);
      result.transform(mattrans);
      break;
    case RFont.RIGHT:
      r = result.getBounds();
      mattrans = new RMatrix();
      mattrans.translate((r.getMinX()-r.getMaxX()),0);
      result.transform(mattrans);
      break;
    case RFont.LEFT:
      break;
    }
    return result;
  }
  
  /**
   * Use this method to draw a character on a certain canvas.
   * @eexample RFont_draw
   * @param character  the character to be drawn
   * @param g   the canvas where to draw
   * @related toShape ( )
   * @related toGroup ( )
   */
  public void draw(char character, PGraphics g) throws RuntimeException{
    this.toShape(character).draw(g);
  }
  
  /**
   * Use this method to draw a character on a certain canvas.
   * @eexample RFont_draw
   * @param text  the string to be drawn
   * @param g  the canvas where to draw
   * @related toShape ( )
   * @related toGroup ( )
   */  
  public void draw(String text, PGraphics g) throws RuntimeException{
    this.toGroup(text).draw(g);
  }
  
  /**
   * Use this method to draw a character on a certain canvas.
   * @eexample RFont_draw
   * @param character char, the character to be drawn
   * @param g  the canvas where to draw
   * @related toShape ( )
   * @related toGroup ( )
   */
  public void draw(char character, PApplet g) throws RuntimeException{
    this.toShape(character).draw(g);
  }
  
  /**
   * Use this method to draw a character on a certain canvas.
   * @eexample RFont_draw
   * @param text  the string to be drawn
   * @param g  the canvas where to draw
   * @related toShape ( )
   * @related toGroup ( )
   */  
  public void draw(String text, PApplet g) throws RuntimeException{
    this.toGroup(text).draw(g);
  }
  
  public void draw(String text) throws RuntimeException{
    this.toGroup(text).draw();
  }
  
  public void draw(char character) throws RuntimeException{
    this.toShape(character).draw();
  }
  
  
  
  private static float midValue(float a, float b) {
    return a + (b - a)/2;
  }
  
  protected static RShape getContourAsShape(Glyph glyph, int startIndex, int count) {
    return getContourAsShape(glyph, startIndex, count, 0);
  }
  
  protected static RShape getContourAsShape(Glyph glyph, int startIndex, int count, float xadv) {
    
    // If this is a single point on it's own, weSystem.out.println("Value of pointx: "+pointx); can't do anything with it
    if (glyph.getPoint(startIndex).endOfContour) {
      return new RShape();
    }
    
    RShape result = new RShape();
    int offset = 0;
    //float originx = 0F,originy = 0F;
    
    String lastCmd = "none";
    while (offset < count) {
      Point point = glyph.getPoint(startIndex + offset%count);
      Point point_plus1 = glyph.getPoint(startIndex + (offset+1)%count);
      Point point_plus2 = glyph.getPoint(startIndex + (offset+2)%count);
      
      float pointx = ((float)point.x + xadv);
      float pointy = ((float)point.y);
      float point_plus1x = ((float)point_plus1.x + xadv);
      float point_plus1y = ((float)point_plus1.y);
      float point_plus2x = ((float)point_plus2.x + xadv);
      float point_plus2y = ((float)point_plus2.y);
      
      if (offset == 0) {
        // move command
        result.addMoveTo(pointx,pointy);
        lastCmd = "move";
      }
    
      if (glyph.getPoint(startIndex + offset).endOfContour) {
        // move command
        //result.addClose();
        System.out.println("onCurve: " + glyph.getPoint(startIndex + offset).onCurve);

        System.out.println("Last cmd: " + lastCmd);
        break;
      }
    
    
      if (point.onCurve && point_plus1.onCurve) {
        // line command
        result.addLineTo(point_plus1x,point_plus1y);
        offset++;
        lastCmd = "lineTo";
    } else if (point.onCurve && !point_plus1.onCurve && point_plus2.onCurve) {
        // This is a curve with no implied points
        // quadratic bezier command
        result.addQuadTo(point_plus1x, point_plus1y, point_plus2x, point_plus2y);
        offset+=2;
        lastCmd = "quadTo";
      } else if (point.onCurve && !point_plus1.onCurve && !point_plus2.onCurve) {
        // This is a curve with one implied point
        // quadratic bezier command avec le endPoint implicit
        result.addQuadTo(point_plus1x, point_plus1y, midValue(point_plus1x, point_plus2x), midValue(point_plus1y, point_plus2y));
        offset+=2;
        lastCmd = "quadTo (implicit one implied)";
      } else if (!point.onCurve && !point_plus1.onCurve) {
        // This is a curve with two implied points
        // quadratic bezier with
        result.addQuadTo(pointx, pointy, midValue(pointx, point_plus1x), midValue(pointy, point_plus1y));
        offset++;
        lastCmd = "quadTo (implicit two implied)";
      } else if (!point.onCurve && point_plus1.onCurve) {
        // This is a curve with no implied points
        result.addQuadTo(pointx, pointy, point_plus1x, point_plus1y);
        offset++;
        lastCmd = "quadTo (point not on curve)";
      } else {
        System.out.println("drawGlyph case not catered for!!");
        break;
      }
    }
    
    //Point point_first = glyph.getPoint(startIndex);
    //Point point_last = glyph.getPoint(startIndex + count-1);
    //if (point_first.x == point_last.x && point_first.y == point_last.y) {
    //if (glyph.getPoint(startIndex + count-1).endOfContour) {
    //result.addClose();
    //}
    
    return result;
  }
  
  protected static RShape getGlyphAsShape(Font font, Glyph glyph, int glyphIndex) {
    return getGlyphAsShape(font,glyph,glyphIndex,0);
  }
  
  protected static RShape getGlyphAsShape(Font font, Glyph glyph, int glyphIndex,float xadv) {
    
    RShape result = new RShape();
    int firstIndex = 0;
    int count = 0;
    int i;
    
    if (glyph != null) {
      for (i = 0; i < glyph.getPointCount(); i++) {
        count++;
        if (glyph.getPoint(i).endOfContour) {
          result.addShape(getContourAsShape(glyph, firstIndex, count, xadv));
          firstIndex = i + 1;
          count = 0;
        }
      }
    }
    
    return result;
  }
  
  protected static RShape getGlyphAsShape(Font font, Glyph glyph, int glyphIndex, SingleSubst arabInitSubst, SingleSubst arabMediSubst, SingleSubst arabTermSubst) {
    return getGlyphAsShape(font, glyph, glyphIndex, arabInitSubst, arabMediSubst, arabTermSubst, 0);
  }
  
  protected static RShape getGlyphAsShape(Font font, Glyph glyph, int glyphIndex, SingleSubst arabInitSubst, SingleSubst arabMediSubst, SingleSubst arabTermSubst, float xadv) {
    
    RShape result = new RShape();
    boolean substituted = false;
    
    // arabic = "initial | medial | terminal | isolated"
    int arabInitGlyphIndex = glyphIndex;
    int arabMediGlyphIndex = glyphIndex;
    int arabTermGlyphIndex = glyphIndex;
    if (arabInitSubst != null) {
      arabInitGlyphIndex = arabInitSubst.substitute(glyphIndex);
    }
    if (arabMediSubst != null) {
      arabMediGlyphIndex = arabMediSubst.substitute(glyphIndex);
    }
    if (arabTermSubst != null) {
      arabTermGlyphIndex = arabTermSubst.substitute(glyphIndex);
    }
    
    if (arabInitGlyphIndex != glyphIndex) {
      result.addShape(getGlyphAsShape(font,font.getGlyph(arabInitGlyphIndex),arabInitGlyphIndex));
      substituted = true;
    }
    
    if (arabMediGlyphIndex != glyphIndex) {
      result.addShape(getGlyphAsShape(font,font.getGlyph(arabMediGlyphIndex),arabMediGlyphIndex));
      substituted = true;
    }
    
    if (arabTermGlyphIndex != glyphIndex) {
      result.addShape(getGlyphAsShape(font,font.getGlyph(arabTermGlyphIndex),arabTermGlyphIndex));
      substituted = true;
    }
    
    if (substituted) {
      result.addShape(getGlyphAsShape(font,glyph,glyphIndex));
    } else {
      result.addShape(getGlyphAsShape(font,glyph,glyphIndex));
    }
    
    return result;
  }
}
