/*
 * $Id: CMapFormat4.java,v 1.3 2011-04-15 15:44:14 xphc Exp $
 *
 * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle,
 * Santa Clara, California 95054, U.S.A. All rights reserved.
 *
 * This library 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 library 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 library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */

package com.sun.pdfview.font.ttf;

import java.nio.ByteBuffer;
import java.util.Collections;
import java.util.Iterator;
import java.util.SortedMap;
import java.util.TreeMap;

/**
 *
 * @author  jkaplan
 */
public class CMapFormat4 extends CMap {
   
    /**
     * The segments and associated data can be a char[] or an Integer
     */
    public SortedMap<Segment,Object> segments;
    
    /** Creates a new instance of CMapFormat0 */
    protected CMapFormat4(short language) {
        super((short) 4, language);
    
        this.segments = Collections.synchronizedSortedMap(new TreeMap<Segment,Object>());
        
        char[] map = new char[1];
        map[0] = (char) 0;
        addSegment((short) 0xffff, (short) 0xffff, map);
    }
    
    /**
     * Add a segment with a map 
     */
    public void addSegment(short startCode, short endCode, char[] map) {
        if (map.length != (endCode - startCode) + 1) {
            throw new IllegalArgumentException("Wrong number of entries in map");
        }
        
        Segment s = new Segment(startCode, endCode, true);
        // make sure we remove any old entries
        this.segments.remove(s);
        this.segments.put(s, map);
    }
    
    /**
     * Add a segment with an idDelta
     */
    public void addSegment(short startCode, short endCode, short idDelta) {
        Segment s = new Segment(startCode, endCode, false);
        // make sure we remove any old entries
        this.segments.remove(s);
        this.segments.put(s, Integer.valueOf(idDelta));
    }
    
    /**
     * Remove a segment
     */
    public void removeSegment(short startCode, short endCode) {
        Segment s = new Segment(startCode, endCode, true);
        this.segments.remove(s);
    }
    
    /**
     * Get the length of this table
     */
    @Override
	public short getLength() {
        // start with the size of the fixed header
        short size = 16;
        
        // add the size of each segment header
        size += this.segments.size() * 8;
        
        // add the total number of mappings times the size of a mapping
        for (Iterator i = this.segments.keySet().iterator(); i.hasNext();) {
            Segment s = (Segment) i.next();
            
            // see if there's a map
            if (s.hasMap) {
                // if there is, add its size
                char[] map = (char[]) this.segments.get(s);
                size += map.length * 2;
            }
        }
        
        return size;
    }
    
    /** 
     * Cannot map from a byte
     */
    @Override
	public byte map(byte src) {
        char c = map((char) src);
        if (c < Byte.MIN_VALUE || c > Byte.MAX_VALUE) {
            // out of range
            return 0;
        }
    
        return (byte) c;
    }
    
    /**
     * Map from char
     */
    @Override
	public char map(char src) {
        // find first segment with endcode > src
        for (Iterator i = this.segments.keySet().iterator(); i.hasNext();) {
            Segment s = (Segment) i.next();
            
            if (s.endCode >= src) {
                // are we within range?
                if (s.startCode <= src) {
                    if (s.hasMap) {
                        // return the index of this character in 
                        // the segment's map
                        char[] map = (char[]) this.segments.get(s);
                        return map[src - s.startCode];
                    } else {
                        // return the character code + idDelta
                        Integer idDelta = (Integer) this.segments.get(s);
                        return (char) (src + idDelta.intValue());
                    }
                } else {
                    // undefined character
                    return (char) 0;
                }
            }
        }
        
        // shouldn't get here!
        return (char) 0;
    }
    
    /**
     * Get the src code which maps to the given glyphID
     */
    @Override
	public char reverseMap(short glyphID) {
        // look at each segment
        for (Iterator i = this.segments.keySet().iterator(); i.hasNext();) {
            Segment s = (Segment) i.next();
            
            // see if we have a map or a delta
            if (s.hasMap) {
                char[] map = (char[]) this.segments.get(s);
                
                // if we have a map, we have to iterate through it
                for (int c = 0; c < map.length; c++) {
                    if (map[c] == glyphID) {
                        return (char) (s.startCode + c);
                    }
                }
            } else {
                Integer idDelta = (Integer) this.segments.get(s);
                
                // we can do the math to see if we're in range
                int start = s.startCode + idDelta.intValue();
                int end = s.endCode + idDelta.intValue();
                
                if (glyphID >= start && glyphID <= end) {
                    // we're in the range
                    return (char) (glyphID - idDelta.intValue());
                }
            }
        }
        
        // not found!
        return (char) 0;
    }
    
    
    /**
     * Get the data in this map as a ByteBuffer
     */
    @Override
	public void setData(int length, ByteBuffer data) {
        // read the table size values
        short segCount = (short) (data.getShort() / 2);
        short searchRange = data.getShort();
        short entrySelector = data.getShort();
        short rangeShift = data.getShort();
    
        // create arrays to store segment info
        short[] endCodes = new short[segCount];
        short[] startCodes = new short[segCount];
        short[] idDeltas = new short[segCount];
        short[] idRangeOffsets = new short[segCount];
          
        // the start of the glyph array
        int glyphArrayPos = 16 + (8 * segCount);
        
        // read the endCodes
        for (int i = 0; i < segCount; i++) {
           endCodes[i] = data.getShort();
        }
        
        // read the pad
        data.getShort();
        
        // read the start codes
        for (int i = 0; i < segCount; i++) {
            startCodes[i] = data.getShort();
        }
        
        // read the idDeltas
        for (int i = 0; i < segCount; i++) {
            idDeltas[i] = data.getShort();
        }
        
        // read the id range offsets
        for (int i = 0; i < segCount; i++) {
            idRangeOffsets[i] = data.getShort();
            
            // calculate the actual offset
            if (idRangeOffsets[i] <= 0) {
                // the easy way
                addSegment(startCodes[i], endCodes[i], idDeltas[i]);
            } else {
                // find the start of the data segment
                int offset = (data.position() - 2) + idRangeOffsets[i];
            
                // get the number of entries in the map
                int size = (endCodes[i] - startCodes[i]) + 1;
            
                // allocate the actual map
                char[] map = new char[size];
                
                // remember our offset
                data.mark();
                 
                // read the mappings    
                for (int c = 0; c < size; c++) {
                    data.position(offset + (c * 2));
                    map[c] = data.getChar();
                }
      
                // reset the position
                data.reset();
                
                addSegment(startCodes[i], endCodes[i], map);
            }
        }       
    }
    
    /** 
     * Get the data in the map as a byte buffer
     */
    @Override
	public ByteBuffer getData() {
        ByteBuffer buf = ByteBuffer.allocate(getLength());
    
        // write the header
        buf.putShort(getFormat());
        buf.putShort(getLength());
        buf.putShort(getLanguage());
        
        // write the various values
        buf.putShort((short) (getSegmentCount() * 2));
        buf.putShort(getSearchRange());
        buf.putShort(getEntrySelector());
        buf.putShort(getRangeShift());
        
        // write the endCodes
        for (Iterator i = this.segments.keySet().iterator(); i.hasNext();) {
            Segment s = (Segment) i.next();
            buf.putShort((short) s.endCode);
        }
        
        // write the pad
        buf.putShort((short) 0);
        
        // write the startCodes
        for (Iterator i = this.segments.keySet().iterator(); i.hasNext();) {
            Segment s = (Segment) i.next();
            buf.putShort((short) s.startCode);
        }
        
        // write the idDeltas for segments using deltas
        for (Iterator i = this.segments.keySet().iterator(); i.hasNext();) {
            Segment s = (Segment) i.next();
            
            if (!s.hasMap) {
                Integer idDelta = (Integer) this.segments.get(s);
                buf.putShort(idDelta.shortValue());
            } else {
                buf.putShort((short) 0);
            }
        }
        
        // the start of the glyph array
        int glyphArrayOffset = 16 + (8 * getSegmentCount());
        
        // write the idRangeOffsets and maps for segments using maps
        for (Iterator i = this.segments.keySet().iterator(); i.hasNext();) {
            Segment s = (Segment) i.next();
            
            if (s.hasMap) {
                // first set the offset, which is the number of bytes from the
                // current position to the current offset
                buf.putShort((short) (glyphArrayOffset - buf.position()));
                
                // remember the current position
                buf.mark();
                
                // move the position to the offset
                buf.position(glyphArrayOffset);
                
                // now write the map
                char[] map = (char[]) this.segments.get(s);
                for (int c = 0; c < map.length; c++) {
                    buf.putChar(map[c]);
                }
                
                // reset the data pointer
                buf.reset();
                
                // update the offset
                glyphArrayOffset += map.length * 2;
            } else {
                buf.putShort((short) 0);
            }
        }

        // make sure we are at the end of the buffer before we flip
        buf.position(glyphArrayOffset);
        
        // reset the data pointer
        buf.flip();
        
        return buf;
    }
    
    /**
     * Get the segment count
     */
    public short getSegmentCount() {
        return (short) this.segments.size();
    }
    
    /**
     * Get the search range
     */
    public short getSearchRange() {
        double pow = Math.floor(Math.log(getSegmentCount()) / Math.log(2));
        double pow2 = Math.pow(2, pow);
        
        return (short) (2 * pow2);
    }
    
    /**
     * Get the entry selector
     */
    public short getEntrySelector() {
        int sr2 = getSearchRange() / 2;
        return (short) (Math.log(sr2) / Math.log(2));
    }
    
    /**
     * Get the rangeShift()
     */
    public short getRangeShift() {
        return (short) ((2 * getSegmentCount()) - getSearchRange());
    }
    
    /** Get a pretty string */
    @Override public String toString() {
        StringBuffer buf = new StringBuffer();
        String indent = "        ";
        
        buf.append(super.toString());
        buf.append(indent + "SegmentCount : " + getSegmentCount() + "\n");
        buf.append(indent + "SearchRange  : " + getSearchRange() + "\n");
        buf.append(indent + "EntrySelector: " + getEntrySelector() + "\n");
        buf.append(indent + "RangeShift   : " + getRangeShift() + "\n");
        
        for (Iterator i = this.segments.keySet().iterator(); i.hasNext();) {
            Segment s = (Segment) i.next();
            
            buf.append(indent);
            buf.append("Segment: " + Integer.toHexString(s.startCode));
            buf.append("-" + Integer.toHexString(s.endCode) + " ");
            buf.append("hasMap: " + s.hasMap + " ");
            
            if (!s.hasMap) {
                buf.append("delta: " + this.segments.get(s));
            }
            
            buf.append("\n");
        }
        
        return buf.toString();
    }
    
    class Segment implements Comparable {
        /** the end code (highest code in this segment) */
        int endCode;
        
        /** the start code (lowest code in this segment) */
        int startCode;
        
        /** whether it is a map or a delta */
        boolean hasMap;
        
        /** Create a new segment */
        public Segment(short startCode, short endCode, boolean hasMap) {
            // convert from unsigned short
            this.endCode   = (0xffff & endCode);
            this.startCode = (0xffff & startCode);
            
            this.hasMap = hasMap;
        }
        
        /** Equals based on compareTo (only compares endCode) */
        @Override public boolean equals(Object o) {
            return (compareTo(o) == 0);
        }
        
        /** Segments sort by increasing endCode */
        @Override
		public int compareTo(Object o) {
            if (!(o instanceof Segment)) {
                return -1;
            }
            
            Segment s = (Segment) o;
        
            // if regions overlap at all, declare the segments equal,
            // to avoid overlap in the segment list
            if (((s.endCode >= this.startCode) && (s.endCode <= this.endCode)) ||
                ((s.startCode >= this.startCode) && (s.startCode <= this.endCode))) {
                return 0;
            } if (this.endCode > s.endCode) {
                return 1;
            } else if (this.endCode < s.endCode) {
                return -1;
            } else {
                return 0;
            }
        }
    }
}