/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.fontbox.cff;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.fontbox.cff.IndexData;
import org.apache.fontbox.cff.charset.CFFCharset;
import org.apache.fontbox.cff.encoding.CFFEncoding;

/**
 * This class represents a CFF/Type2 Font.
 * 
 * @author Villu Ruusmann
 * @version $Revision$
 */
public class CFFFont
{

	private String fontname = null;
	private Map<String, Object> topDict = new LinkedHashMap<String, Object>();
	private Map<String, Object> privateDict = new LinkedHashMap<String, Object>();
	private CFFEncoding fontEncoding = null;
	private CFFCharset fontCharset = null;
	private Map<String, byte[]> charStringsDict = new LinkedHashMap<String, byte[]>();
	private IndexData globalSubrIndex = null;
	private IndexData localSubrIndex = null;

	/**
	 * The name of the font.
	 * @return the name of the font
	 */
	public String getName()
	{
		return fontname;
	}

	/**
	 * Sets the name of the font.
	 * @param name the name of the font
	 */
	public void setName(String name)
	{
		fontname = name;
	}

	/**
	 * Returns the value for the given name from the dictionary.
	 * @param name the name of the value
	 * @return the value of the name if available
	 */
	public Object getProperty(String name)
	{
		Object topDictValue = topDict.get(name);
		if (topDictValue != null)
		{
			return topDictValue;
		}
		Object privateDictValue = privateDict.get(name);
		if (privateDictValue != null)
		{
			return privateDictValue;
		}
		return null;
	}

	/**
	 * Adds the given key/value pair to the top dictionary. 
	 * @param name the given key
	 * @param value the given value
	 */
	public void addValueToTopDict(String name, Object value)
	{
		if (value != null)
		{
			topDict.put(name, value);
		}
	}
	/** 
	 * Returns the top dictionary.
	 * @return the dictionary
	 */
	public Map<String, Object> getTopDict()
	{
		return topDict;
	}

	/**
	 * Adds the given key/value pair to the private dictionary. 
	 * @param name the given key
	 * @param value the given value
	 */
	public void addValueToPrivateDict(String name, Object value)
	{
		if (value != null)
		{
			privateDict.put(name, value);
		}
	}
	/** 
	 * Returns the private dictionary.
	 * @return the dictionary
	 */
	public Map<String, Object> getPrivateDict()
	{
		return privateDict;
	}

	/**
	 * Get the mapping (code/SID/charname/bytes) for this font.
	 * @return mappings for codes &lt; 256 and for codes &gt;= 256
	 */
	public Collection<Mapping> getMappings()
	{
		List<Mapping> mappings = new ArrayList<Mapping>();
		Set<String> mappedNames = new HashSet<String>();
		for (CFFEncoding.Entry entry : fontEncoding.getEntries())
		{
			String charName = fontCharset.getName(entry.getSID());
			// Predefined encoding
			if (charName == null)
			{
				continue;
			}
			byte[] bytes = charStringsDict.get(charName);
			if (bytes == null)
			{
				continue;
			}
			Mapping mapping = new Mapping();
			mapping.setCode(entry.getCode());
			mapping.setSID(entry.getSID());
			mapping.setName(charName);
			mapping.setBytes(bytes);
			mappings.add(mapping);
			mappedNames.add(charName);
		}
		if (fontEncoding instanceof CFFParser.EmbeddedEncoding)
		{
			CFFParser.EmbeddedEncoding embeddedEncoding = (CFFParser.EmbeddedEncoding)fontEncoding;

			for (CFFParser.EmbeddedEncoding.Supplement supplement : embeddedEncoding.getSupplements())
			{
				String charName = fontCharset.getName(supplement.getGlyph());
				if (charName == null)
				{
					continue;
				}
				byte[] bytes = charStringsDict.get(charName);
				if (bytes == null)
				{
					continue;
				}
				Mapping mapping = new Mapping();
				mapping.setCode(supplement.getCode());
				mapping.setSID(supplement.getGlyph());
				mapping.setName(charName);
				mapping.setBytes(bytes);
				mappings.add(mapping);
				mappedNames.add(charName);
			}
		}
		// XXX
		int code = 256;
		for (CFFCharset.Entry entry : fontCharset.getEntries())
		{
			String name = entry.getName();
			if (mappedNames.contains(name))
			{
				continue;
			}
			byte[] bytes = this.charStringsDict.get(name);
			if (bytes == null)
			{
				continue;
			}
			Mapping mapping = new Mapping();
			mapping.setCode(code++);
			mapping.setSID(entry.getSID());
			mapping.setName(name);
			mapping.setBytes(bytes);

			mappings.add(mapping);

			mappedNames.add(name);
		}
		return mappings;
	}

	/**
	 * Return the Width value of the given Glyph identifier
	 * 
	 * @param SID
	 * @return -1 if the SID is missing from the Font.
	 * @throws IOException
	 */
	public int getWidth(int SID) throws IOException {
		int nominalWidth = privateDict.containsKey("nominalWidthX") ? ((Number)privateDict.get("nominalWidthX")).intValue() : 0;
		int defaultWidth = privateDict.containsKey("defaultWidthX") ? ((Number)privateDict.get("defaultWidthX")).intValue() : 1000 ;
		
		for (Mapping m : getMappings() ){
			if (m.getSID() == SID) {

				CharStringRenderer csr = null;
				if (((Number)getProperty("CharstringType")).intValue() == 2 ) {
					List<Object> lSeq = m.toType2Sequence();
					csr = new CharStringRenderer(false);
					csr.render(lSeq);
				} else {
					List<Object> lSeq = m.toType1Sequence();
					csr = new CharStringRenderer();
					csr.render(lSeq);
				}

				// ---- If the CharString has a Width nominalWidthX must be added, 
				//	    otherwise it is the default width.
				return csr.getWidth() != 0 ? csr.getWidth() + nominalWidth : defaultWidth;
			}
		}

		// ---- SID Width not found, return the nodef width
		return getNotDefWidth(defaultWidth, nominalWidth);
	}
	
	protected int getNotDefWidth(int defaultWidth, int nominalWidth) throws IOException {
		CharStringRenderer csr;
		byte[] glyphDesc = this.getCharStringsDict().get(".notdef");
		if (((Number)getProperty("CharstringType")).intValue() == 2 ) {
			Type2CharStringParser parser = new Type2CharStringParser();
			List<Object> lSeq = parser.parse(glyphDesc, getGlobalSubrIndex(), getLocalSubrIndex());
			csr = new CharStringRenderer(false);
			csr.render(lSeq);
		} else {
			Type1CharStringParser parser = new Type1CharStringParser();
			List<Object> lSeq = parser.parse(glyphDesc, getLocalSubrIndex());
			csr = new CharStringRenderer();
			csr.render(lSeq);
		}
		return csr.getWidth() != 0 ? csr.getWidth() + nominalWidth : defaultWidth;
	}
	
	/**
	 * Returns the CFFEncoding of the font.
	 * @return the encoding
	 */
	public CFFEncoding getEncoding()
	{
		return fontEncoding;
	}

	/**
	 * Sets the CFFEncoding of the font.
	 * @param encoding the given CFFEncoding
	 */
	public void setEncoding(CFFEncoding encoding)
	{
		fontEncoding = encoding;
	}

	/**
	 * Returns the CFFCharset of the font.
	 * @return the charset
	 */
	public CFFCharset getCharset()
	{
		return fontCharset;
	}

	/**
	 * Sets the CFFCharset of the font.
	 * @param charset the given CFFCharset
	 */
	public void setCharset(CFFCharset charset)
	{
		fontCharset = charset;
	}

	/** 
	 * Returns the character strings dictionary.
	 * @return the dictionary
	 */
	public Map<String, byte[]> getCharStringsDict()
	{
		return charStringsDict;
	}

	/**
	 * Creates a CharStringConverter for this font.
	 * @return the new CharStringConverter
	 */
	public CharStringConverter createConverter()
	{
		Number defaultWidthX = (Number) getProperty("defaultWidthX");
		Number nominalWidthX = (Number) getProperty("nominalWidthX");
		return new CharStringConverter(defaultWidthX.intValue(), nominalWidthX.intValue());
	}

	/**
	 * Creates a CharStringRenderer for this font.
	 * @return the new CharStringRenderer
	 */
	public CharStringRenderer createRenderer()
	{
		return new CharStringRenderer();
	}

	/**
	 * {@inheritDoc}
	 */
	public String toString()
	{
		return getClass().getName() + "[name=" + fontname + ", topDict=" + topDict
				+ ", privateDict=" + privateDict + ", encoding=" + fontEncoding
				+ ", charset=" + fontCharset + ", charStringsDict="
				+ charStringsDict + "]";
	}


	/**
	 * Sets the global subroutine index data.
	 * @param globalSubrIndex the IndexData object containing the global subroutines 
	 */
	public void setGlobalSubrIndex(IndexData globalSubrIndex) {
		this.globalSubrIndex = globalSubrIndex;
	}

	/** 
	 * Returns the global subroutine index data.
	 * @return the dictionary
	 */
	public IndexData getGlobalSubrIndex() {
		return globalSubrIndex;
	}

	/** 
	 * Returns the local subroutine index data.
	 * @return the dictionary
	 */
	public IndexData getLocalSubrIndex() {
		return localSubrIndex;
	}

	/**
	 * Sets the local subroutine index data.
	 * @param localSubrIndex the IndexData object containing the local subroutines 
	 */
	public void setLocalSubrIndex(IndexData localSubrIndex) {
		this.localSubrIndex = localSubrIndex;	
	}

	/**
	 * This class is used for the font mapping.
	 *
	 */
	public class Mapping
	{
		private int mappedCode;
		private int mappedSID;
		private String mappedName;
		private byte[] mappedBytes;

		/**
		 * Converts the mapping into a Type1-sequence.
		 * @return the Type1-sequence
		 * @throws IOException if an error occurs during reading
		 */
		public List<Object> toType1Sequence() throws IOException
		{
			CharStringConverter converter = createConverter();
			return converter.convert(toType2Sequence());
		}

		/**
		 * Converts the mapping into a Type2-sequence.
		 * @return the Type2-sequence
		 * @throws IOException if an error occurs during reading
		 */
		public List<Object> toType2Sequence() throws IOException
		{
			Type2CharStringParser parser = new Type2CharStringParser();
			return parser.parse(getBytes(), getGlobalSubrIndex(), getLocalSubrIndex());
		}

		/**
		 * Gets the value for the code.
		 * @return the code
		 */
		public int getCode()
		{
			return mappedCode;
		}

		private void setCode(int code)
		{
			mappedCode = code;
		}

		/**
		 * Gets the value for the SID.
		 * @return the SID
		 */
		public int getSID()
		{
			return mappedSID;
		}

		private void setSID(int sid)
		{
			this.mappedSID = sid;
		}

		/**
		 * Gets the value for the name.
		 * @return the name
		 */
		public String getName()
		{
			return mappedName;
		}

		private void setName(String name)
		{
			this.mappedName = name;
		}

		/**
		 * Gets the value for the bytes.
		 * @return the bytes
		 */
		public byte[] getBytes()
		{
			return mappedBytes;
		}

		private void setBytes(byte[] bytes)
		{
			this.mappedBytes = bytes;
		}
	}
}