/*
 * 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.
 */

/* $Id: SingleByteFont.java 1695082 2015-08-10 14:15:48Z rmeyer $ */

package org.apache.fop.fonts;

import java.awt.Rectangle;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.apache.xmlgraphics.fonts.Glyphs;

import org.apache.fop.apps.io.InternalResourceResolver;
import org.apache.fop.fonts.truetype.OpenFont.PostScriptVersion;
import org.apache.fop.util.CharUtilities;

/**
 * Generic SingleByte font
 */
public class SingleByteFont extends CustomFont {

    /** logger */
    private  static Log log = LogFactory.getLog(SingleByteFont.class);

    protected SingleByteEncoding mapping;
    private boolean useNativeEncoding;

    protected int[] width;

    private Rectangle[] boundingBoxes;

    private Map<Character, UnencodedCharacter> unencodedCharacters;
    private List<SimpleSingleByteEncoding> additionalEncodings;
    private Map<Character, Character> alternativeCodes;

    private PostScriptVersion ttPostScriptVersion;

    private int usedGlyphsCount;
    private LinkedHashMap<Integer, String> usedGlyphNames;
    private Map<Integer, Integer> usedGlyphs;
    private Map<Integer, Character> usedCharsIndex;
    private Map<Character, Integer> charGIDMappings;

    public SingleByteFont(InternalResourceResolver resourceResolver) {
        super(resourceResolver);
        setEncoding(CodePointMapping.WIN_ANSI_ENCODING);
    }

    public SingleByteFont(InternalResourceResolver resourceResolver, EmbeddingMode embeddingMode) {
        this(resourceResolver);
        setEmbeddingMode(embeddingMode);
        if (embeddingMode != EmbeddingMode.FULL) {
            usedGlyphNames = new LinkedHashMap<Integer, String>();
            usedGlyphs = new HashMap<Integer, Integer>();
            usedCharsIndex = new HashMap<Integer, Character>();
            charGIDMappings = new HashMap<Character, Integer>();

            // The zeroth value is reserved for .notdef
            usedGlyphs.put(0, 0);
            usedGlyphsCount++;
        }
    }

    /** {@inheritDoc} */
    public boolean isEmbeddable() {
        return (!(getEmbedFileURI() == null
                && getEmbedResourceName() == null));
    }

    /** {@inheritDoc} */
    public boolean isSubsetEmbedded() {
        return getEmbeddingMode() != EmbeddingMode.FULL;
    }

    /** {@inheritDoc} */
    public String getEncodingName() {
        return this.mapping.getName();
    }

    /**
     * Returns the code point mapping (encoding) of this font.
     * @return the code point mapping
     */
    public SingleByteEncoding getEncoding() {
        return this.mapping;
    }

    /** {@inheritDoc} */
    public int getWidth(int i, int size) {
        if (i < 256) {
            int idx = i - getFirstChar();
            if (idx >= 0 && idx < width.length) {
                return size * width[idx];
            }
        } else if (this.additionalEncodings != null) {
            int encodingIndex = (i / 256) - 1;
            SimpleSingleByteEncoding encoding = getAdditionalEncoding(encodingIndex);
            int codePoint = i % 256;
            NamedCharacter nc = encoding.getCharacterForIndex(codePoint);
            UnencodedCharacter uc
                = this.unencodedCharacters.get(Character.valueOf(nc.getSingleUnicodeValue()));
            return size * uc.getWidth();
        }
        return 0;
    }

    /** {@inheritDoc} */
    public int[] getWidths() {
        int[] arr = new int[width.length];
        System.arraycopy(width, 0, arr, 0, width.length);
        return arr;
    }

    public Rectangle getBoundingBox(int glyphIndex, int size) {
        Rectangle bbox = null;
        if (glyphIndex < 256) {
            int idx = glyphIndex - getFirstChar();
            if (idx >= 0 && idx < boundingBoxes.length) {
                bbox =  boundingBoxes[idx];
            }
        } else if (this.additionalEncodings != null) {
            int encodingIndex = (glyphIndex / 256) - 1;
            SimpleSingleByteEncoding encoding = getAdditionalEncoding(encodingIndex);
            int codePoint = glyphIndex % 256;
            NamedCharacter nc = encoding.getCharacterForIndex(codePoint);
            UnencodedCharacter uc = this.unencodedCharacters.get(Character.valueOf(nc.getSingleUnicodeValue()));
            bbox = uc.getBBox();
        }
        return bbox == null ? null : new Rectangle(bbox.x * size, bbox.y * size, bbox.width * size, bbox.height * size);
    }

    /**
     * Lookup a character using its alternative names. If found, cache it so we
     * can speed up lookups.
     * @param c the character
     * @return the suggested alternative character present in the font
     */
    private char findAlternative(char c) {
        char d;
        if (alternativeCodes == null) {
            alternativeCodes = new java.util.HashMap<Character, Character>();
        } else {
            Character alternative = alternativeCodes.get(c);
            if (alternative != null) {
                return alternative;
            }
        }
        String charName = Glyphs.charToGlyphName(c);
        String[] charNameAlternatives = Glyphs.getCharNameAlternativesFor(charName);
        if (charNameAlternatives != null && charNameAlternatives.length > 0) {
            for (int i = 0; i < charNameAlternatives.length; i++) {
                if (log.isDebugEnabled()) {
                    log.debug("Checking alternative for char " + c + " (charname="
                            + charName + "): " + charNameAlternatives[i]);
                }
                String s = Glyphs.getUnicodeSequenceForGlyphName(charNameAlternatives[i]);
                if (s != null) {
                    d = lookupChar(s.charAt(0));
                    if (d != SingleByteEncoding.NOT_FOUND_CODE_POINT) {
                        alternativeCodes.put(c, d);
                        return d;
                    }
                }
            }
        }

        return SingleByteEncoding.NOT_FOUND_CODE_POINT;
    }

    private char lookupChar(char c) {
        char d = mapping.mapChar(c);
        if (d != SingleByteEncoding.NOT_FOUND_CODE_POINT) {
            return d;
        }

        // Check unencoded characters which are available in the font by
        // character name
        d = mapUnencodedChar(c);
        return d;
    }

    private boolean isSubset() {
        return getEmbeddingMode() == EmbeddingMode.SUBSET;
    }

    /** {@inheritDoc} */
    @Override
    public char mapChar(char c) {
        notifyMapOperation();
        char d = lookupChar(c);
        if (d == SingleByteEncoding.NOT_FOUND_CODE_POINT) {
            // Check for alternative
            d = findAlternative(c);
            if (d != SingleByteEncoding.NOT_FOUND_CODE_POINT) {
                return d;
            } else {
                this.warnMissingGlyph(c);
                return Typeface.NOT_FOUND;
            }
        }
        if (isEmbeddable() && isSubset()) {
            mapChar(d, c);
        }
        return d;
    }

    private int mapChar(int glyphIndex, char unicode) {
        // Reencode to a new subset font or get the reencoded value
        // IOW, accumulate the accessed characters and build a character map for them
        Integer subsetCharSelector = usedGlyphs.get(glyphIndex);
        if (subsetCharSelector == null) {
            int selector = usedGlyphsCount;
            usedGlyphs.put(glyphIndex, selector);
            usedCharsIndex.put(selector, unicode);
            charGIDMappings.put(unicode, glyphIndex);
            usedGlyphsCount++;
            return selector;
        } else {
            return subsetCharSelector;
        }
    }

    private char getUnicode(int index) {
        Character mapValue = usedCharsIndex.get(index);
        if (mapValue != null) {
            return mapValue.charValue();
        } else {
            return CharUtilities.NOT_A_CHARACTER;
        }
    }

    private char mapUnencodedChar(char ch) {
        if (this.unencodedCharacters != null) {
            UnencodedCharacter unencoded = this.unencodedCharacters.get(Character.valueOf(ch));
            if (unencoded != null) {
                if (this.additionalEncodings == null) {
                    this.additionalEncodings = new ArrayList<SimpleSingleByteEncoding>();
                }
                SimpleSingleByteEncoding encoding = null;
                char mappedStart = 0;
                int additionalsCount = this.additionalEncodings.size();
                for (int i = 0; i < additionalsCount; i++) {
                    mappedStart += 256;
                    encoding = getAdditionalEncoding(i);
                    char alt = encoding.mapChar(ch);
                    if (alt != 0) {
                        return (char)(mappedStart + alt);
                    }
                }
                if (encoding != null && encoding.isFull()) {
                    encoding = null;
                }
                if (encoding == null) {
                    encoding = new SimpleSingleByteEncoding(
                            getFontName() + "EncodingSupp" + (additionalsCount + 1));
                    this.additionalEncodings.add(encoding);
                    mappedStart += 256;
                }
                return (char)(mappedStart + encoding.addCharacter(unencoded.getCharacter()));
            }
        }
        return 0;
    }

    /** {@inheritDoc} */
    @Override
    public boolean hasChar(char c) {
        char d = mapping.mapChar(c);
        if (d != SingleByteEncoding.NOT_FOUND_CODE_POINT) {
            return true;
        }
        //Check unencoded characters which are available in the font by character name
        d = mapUnencodedChar(c);
        if (d != SingleByteEncoding.NOT_FOUND_CODE_POINT) {
            return true;
        }
        // Check if an alternative exists
        d = findAlternative(c);
        if (d != SingleByteEncoding.NOT_FOUND_CODE_POINT) {
            return true;
        }
        return false;
    }

    /* ---- single byte font specific setters --- */

    /**
     * Updates the mapping variable based on the encoding.
     * @param encoding the name of the encoding
     */
    protected void updateMapping(String encoding) {
        try {
            this.mapping = CodePointMapping.getMapping(encoding);
        } catch (UnsupportedOperationException e) {
            log.error("Font '" + super.getFontName() + "': " + e.getMessage());
        }
    }

    /**
     * Sets the encoding of the font.
     * @param encoding the encoding (ex. "WinAnsiEncoding" or "SymbolEncoding")
     */
    public void setEncoding(String encoding) {
        updateMapping(encoding);
    }

    /**
     * Sets the encoding of the font.
     * @param encoding the encoding information
     */
    public void setEncoding(CodePointMapping encoding) {
        this.mapping = encoding;
    }

    /**
     * Controls whether the font is configured to use its native encoding or if it
     * may need to be re-encoded for the target format.
     * @param value true indicates that the configured encoding is the font's native encoding
     */
    public void setUseNativeEncoding(boolean value) {
        this.useNativeEncoding = value;
    }

    /**
     * Indicates whether this font is configured to use its native encoding. This
     * method is used to determine whether the font needs to be re-encoded.
     * @return true if the font uses its native encoding.
     */
    public boolean isUsingNativeEncoding() {
        return this.useNativeEncoding;
    }

    /**
     * Sets a width for a character.
     * @param index index of the character
     * @param w the width of the character
     */
    public void setWidth(int index, int w) {
        if (this.width == null) {
            this.width = new int[getLastChar() - getFirstChar() + 1];
        }
        this.width[index - getFirstChar()] = w;
    }

    public void setBoundingBox(int index, Rectangle bbox) {
        if (this.boundingBoxes == null) {
            this.boundingBoxes = new Rectangle[getLastChar() - getFirstChar() + 1];
        }
        this.boundingBoxes[index - getFirstChar()] = bbox;
    }

    /**
     * Adds an unencoded character (one that is not supported by the primary encoding).
     * @param ch the named character
     * @param width the width of the character
     */
    public void addUnencodedCharacter(NamedCharacter ch, int width, Rectangle bbox) {
        if (this.unencodedCharacters == null) {
            this.unencodedCharacters = new HashMap<Character, UnencodedCharacter>();
        }
        if (ch.hasSingleUnicodeValue()) {
            UnencodedCharacter uc = new UnencodedCharacter(ch, width, bbox);
            this.unencodedCharacters.put(Character.valueOf(ch.getSingleUnicodeValue()), uc);
        } else {
            //Cannot deal with unicode sequences, so ignore this character
        }
    }

    /**
     * Makes all unencoded characters available through additional encodings. This method
     * is used in cases where the fonts need to be encoded in the target format before
     * all text of the document is processed (for example in PostScript when resource optimization
     * is disabled).
     */
    public void encodeAllUnencodedCharacters() {
        if (this.unencodedCharacters != null) {
            Set<Character> sortedKeys = new TreeSet<Character>(this.unencodedCharacters.keySet());
            for (Character ch : sortedKeys) {
                char mapped = mapChar(ch.charValue());
                assert mapped != Typeface.NOT_FOUND;
            }
        }
    }

    /**
     * Indicates whether the encoding has additional encodings besides the primary encoding.
     * @return true if there are additional encodings.
     */
    public boolean hasAdditionalEncodings() {
        return (this.additionalEncodings != null) && (this.additionalEncodings.size() > 0);
    }

    /**
     * Returns the number of additional encodings this single-byte font maintains.
     * @return the number of additional encodings
     */
    public int getAdditionalEncodingCount() {
        if (hasAdditionalEncodings()) {
            return this.additionalEncodings.size();
        } else {
            return 0;
        }
    }

    /**
     * Returns an additional encoding.
     * @param index the index of the additional encoding
     * @return the additional encoding
     * @throws IndexOutOfBoundsException if the index is out of bounds
     */
    public SimpleSingleByteEncoding getAdditionalEncoding(int index)
            throws IndexOutOfBoundsException {
        if (hasAdditionalEncodings()) {
            return this.additionalEncodings.get(index);
        } else {
            throw new IndexOutOfBoundsException("No additional encodings available");
        }
    }

    /**
     * Returns an array with the widths for an additional encoding.
     * @param index the index of the additional encoding
     * @return the width array
     */
    public int[] getAdditionalWidths(int index) {
        SimpleSingleByteEncoding enc = getAdditionalEncoding(index);
        int[] arr = new int[enc.getLastChar() - enc.getFirstChar() + 1];
        for (int i = 0, c = arr.length; i < c; i++) {
            NamedCharacter nc = enc.getCharacterForIndex(enc.getFirstChar() + i);
            UnencodedCharacter uc = this.unencodedCharacters.get(
                    Character.valueOf(nc.getSingleUnicodeValue()));
            arr[i] = uc.getWidth();
        }
        return arr;
    }

    private static final class UnencodedCharacter {

        private final NamedCharacter character;
        private final int width;
        private final Rectangle bbox;

        public UnencodedCharacter(NamedCharacter character, int width, Rectangle bbox) {
            this.character = character;
            this.width = width;
            this.bbox = bbox;
        }

        public NamedCharacter getCharacter() {
            return this.character;
        }

        public int getWidth() {
            return this.width;
        }

        public Rectangle getBBox() {
            return bbox;
        }

        /** {@inheritDoc} */
        @Override
        public String toString() {
            return getCharacter().toString();
        }
    }

    /**
     * Sets the version of the PostScript table stored in the TrueType font represented by
     * this instance.
     *
     * @param version version of the <q>post</q> table
     */
    public void setTrueTypePostScriptVersion(PostScriptVersion version) {
        ttPostScriptVersion = version;
    }

    /**
     * Returns the version of the PostScript table stored in the TrueType font represented by
     * this instance.
     *
     * @return the version of the <q>post</q> table
     */
    public PostScriptVersion getTrueTypePostScriptVersion() {
        assert getFontType() == FontType.TRUETYPE;
        return ttPostScriptVersion;
    }

    /**
     * Returns a Map of used Glyphs.
     * @return Map Map of used Glyphs
     */
    public Map<Integer, Integer> getUsedGlyphs() {
        return Collections.unmodifiableMap(usedGlyphs);
    }

    public char getUnicodeFromSelector(int selector) {
        return getUnicode(selector);
    }

    public int getGIDFromChar(char ch) {
        return charGIDMappings.get(ch);
    }

    public char getUnicodeFromGID(int glyphIndex) {
        int selector = usedGlyphs.get(glyphIndex);
        return usedCharsIndex.get(selector);
    }

    public void mapUsedGlyphName(int gid, String value) {
        usedGlyphNames.put(gid, value);
    }

    public Map<Integer, String> getUsedGlyphNames() {
        return usedGlyphNames;
    }

    public String getGlyphName(int idx) {
        if (idx < mapping.getCharNameMap().length) {
            return mapping.getCharNameMap()[idx];
        } else {
            int selector = usedGlyphs.get(idx);
            char theChar = usedCharsIndex.get(selector);
            return unencodedCharacters.get(theChar).getCharacter().getName();
        }
    }
}

