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

package org.apache.fop.complexscripts.util;

import java.nio.IntBuffer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

// CSOFF: LineLengthCheck

/**
 * <p>A GlyphSequence encapsulates a sequence of character codes, a sequence of glyph codes,
 * and a sequence of character associations, where, for each glyph in the sequence of glyph
 * codes, there is a corresponding character association. Character associations server to
 * relate the glyph codes in a glyph sequence to the specific characters in an original
 * character code sequence with which the glyph codes are associated.</p>
 *
 * <p>This work was originally authored by Glenn Adams (gadams@apache.org).</p>
 */
public class GlyphSequence implements Cloneable {

    /** default character buffer capacity in case new character buffer is created */
    private static final int DEFAULT_CHARS_CAPACITY = 8;

    /** character buffer */
    private IntBuffer characters;
    /** glyph buffer */
    private IntBuffer glyphs;
    /** association list */
    private List associations;
    /** predications flag */
    private boolean predications;

    protected GlyphSequence unprocessedGS;

    /**
     * Instantiate a glyph sequence, reusing (i.e., not copying) the referenced
     * character and glyph buffers and associations. If characters is null, then
     * an empty character buffer is created. If glyphs is null, then a glyph buffer
     * is created whose capacity is that of the character buffer. If associations is
     * null, then identity associations are created.
     * @param characters a (possibly null) buffer of associated (originating) characters
     * @param glyphs a (possibly null) buffer of glyphs
     * @param associations a (possibly null) array of glyph to character associations
     * @param predications true if predications are enabled
     */
    public GlyphSequence(IntBuffer characters, IntBuffer glyphs, List associations, boolean predications) {
        if (characters == null) {
            characters = IntBuffer.allocate(DEFAULT_CHARS_CAPACITY);
        }
        if (glyphs == null) {
            glyphs = IntBuffer.allocate(characters.capacity());
        }
        if (associations == null) {
            associations = makeIdentityAssociations(characters.limit(), glyphs.limit());
        }
        this.characters = characters;
        this.glyphs = glyphs;
        this.associations = associations;
        this.predications = predications;
        unprocessedGS = this;
    }

    /**
     * Instantiate a glyph sequence, reusing (i.e., not copying) the referenced
     * character and glyph buffers and associations. If characters is null, then
     * an empty character buffer is created. If glyphs is null, then a glyph buffer
     * is created whose capacity is that of the character buffer. If associations is
     * null, then identity associations are created.
     * @param characters a (possibly null) buffer of associated (originating) characters
     * @param glyphs a (possibly null) buffer of glyphs
     * @param associations a (possibly null) array of glyph to character associations
     */
    public GlyphSequence(IntBuffer characters, IntBuffer glyphs, List associations) {
        this (characters, glyphs, associations, false);
    }

    /**
     * Instantiate a glyph sequence using an existing glyph sequence, where the new glyph sequence shares
     * the character array of the existing sequence (but not the buffer object), and creates new copies
     * of glyphs buffer and association list.
     * @param gs an existing glyph sequence
     */
    public GlyphSequence(GlyphSequence gs) {
        this (gs.characters.duplicate(), copyBuffer(gs.glyphs), copyAssociations(gs.associations), gs.predications);
        this.unprocessedGS = gs.unprocessedGS;
    }

    /**
     * Instantiate a glyph sequence using an existing glyph sequence, where the new glyph sequence shares
     * the character array of the existing sequence (but not the buffer object), but uses the specified
     * backtrack, input, and lookahead glyph arrays to populate the glyphs, and uses the specified
     * of glyphs buffer and association list.
     * backtrack, input, and lookahead association arrays to populate the associations.
     * @param gs an existing glyph sequence
     * @param bga backtrack glyph array
     * @param iga input glyph array
     * @param lga lookahead glyph array
     * @param bal backtrack association list
     * @param ial input association list
     * @param lal lookahead association list
     */
    public GlyphSequence(GlyphSequence gs, int[] bga, int[] iga, int[] lga, CharAssociation[] bal, CharAssociation[] ial, CharAssociation[] lal) {
        this (gs.characters.duplicate(), concatGlyphs(bga, iga, lga), concatAssociations(bal, ial, lal), gs.predications);
    }

    /**
     * Obtain reference to underlying character buffer.
     * @return character buffer reference
     */
    public IntBuffer getCharacters() {
        return characters;
    }

    /**
     * Obtain array of characters. If <code>copy</code> is true, then
     * a newly instantiated array is returned, otherwise a reference to
     * the underlying buffer's array is returned. N.B. in case a reference
     * to the undelying buffer's array is returned, the length
     * of the array is not necessarily the number of characters in array.
     * To determine the number of characters, use {@link #getCharacterCount}.
     * @param copy true if to return a newly instantiated array of characters
     * @return array of characters
     */
    public int[] getCharacterArray(boolean copy) {
        if (copy) {
            return toArray(characters);
        } else {
            return characters.array();
        }
    }

    /**
     * Obtain the number of characters in character array, where
     * each character constitutes a unicode scalar value.
     * NB: Supplementary characters (non-BMP code points) count as 1
     * character, not as two UTF-16 code units.
     * @return number of characters available in character array
     */
    public int getCharacterCount() {
        return characters.limit();
    }

    /**
     * Obtain the number of characters in character array, where
     * each character constitutes a UTF-16 character. This means
     * that every non-BMP character is counted as 2 characters.
     * @return number of chars (UTF-16 code units) available in
     * character array
     */
    public int getUTF16CharacterCount() {
        int count = 0;
        for (int ch : characters.array()) {
            count += Character.charCount(ch);
        }
        return count;
    }

    /**
     * Obtain glyph id at specified index.
     * @param index to obtain glyph
     * @return the glyph identifier of glyph at specified index
     * @throws IndexOutOfBoundsException if index is less than zero
     * or exceeds last valid position
     */
    public int getGlyph(int index) throws IndexOutOfBoundsException {
        return glyphs.get(index);
    }

    public int getUnprocessedGlyph(int index) throws IndexOutOfBoundsException {
        return unprocessedGS.getGlyph(index);
    }

    public void setUnprocessedGS(GlyphSequence glyphSequence) {
        unprocessedGS = glyphSequence;
    }

    /**
     * Set glyph id at specified index.
     * @param index to set glyph
     * @param gi glyph index
     * @throws IndexOutOfBoundsException if index is greater or equal to
     * the limit of the underlying glyph buffer
     */
    public void setGlyph(int index, int gi) throws IndexOutOfBoundsException {
        if (gi > 65535) {
            gi = 65535;
        }
        glyphs.put(index, gi);
    }

    /**
     * Obtain reference to underlying glyph buffer.
     * @return glyph buffer reference
     */
    public IntBuffer getGlyphs() {
        return glyphs;
    }

    /**
     * Obtain count glyphs starting at offset. If <code>count</code> is
     * negative, then it is treated as if the number of available glyphs
     * were specified.
     * @param offset into glyph sequence
     * @param count of glyphs to obtain starting at offset, or negative,
     * indicating all avaialble glyphs starting at offset
     * @return glyph array
     */
    public int[] getGlyphs(int offset, int count) {
        int ng = getGlyphCount();
        if (offset < 0) {
            offset = 0;
        } else if (offset > ng) {
            offset = ng;
        }
        if (count < 0) {
            count = ng - offset;
        }
        int[] ga = new int [ count ];
        for (int i = offset, n = offset + count, k = 0; i < n; i++) {
            if (k < ga.length) {
                ga [ k++ ] = glyphs.get(i);
            }
        }
        return ga;
    }

    /**
     * Obtain array of glyphs. If <code>copy</code> is true, then
     * a newly instantiated array is returned, otherwise a reference to
     * the underlying buffer's array is returned. N.B. in case a reference
     * to the undelying buffer's array is returned, the length
     * of the array is not necessarily the number of glyphs in array.
     * To determine the number of glyphs, use {@link #getGlyphCount}.
     * @param copy true if to return a newly instantiated array of glyphs
     * @return array of glyphs
     */
    public int[] getGlyphArray(boolean copy) {
        if (copy) {
            return toArray(glyphs);
        } else {
            return glyphs.array();
        }
    }

    /**
     * Obtain the number of glyphs in glyphs array, where
     * each glyph constitutes a font specific glyph index.
     * @return number of glyphs available in character array
     */
    public int getGlyphCount() {
        return glyphs.limit();
    }

    /**
     * Obtain association at specified index.
     * @param index into associations array
     * @return glyph to character associations at specified index
     * @throws IndexOutOfBoundsException if index is less than zero
     * or exceeds last valid position
     */
    public CharAssociation getAssociation(int index) throws IndexOutOfBoundsException {
        return (CharAssociation) associations.get(index);
    }

    /**
     * Obtain reference to underlying associations list.
     * @return associations list
     */
    public List getAssociations() {
        return associations;
    }

    /**
     * Obtain count associations starting at offset.
     * @param offset into glyph sequence
     * @param count of associations to obtain starting at offset, or negative,
     * indicating all avaialble associations starting at offset
     * @return associations
     */
    public CharAssociation[] getAssociations(int offset, int count) {
        int ng = getGlyphCount();
        if (offset < 0) {
            offset = 0;
        } else if (offset > ng) {
            offset = ng;
        }
        if (count < 0) {
            count = ng - offset;
        }
        CharAssociation[] aa = new CharAssociation [ count ];
        for (int i = offset, n = offset + count, k = 0; i < n; i++) {
            if (k < aa.length) {
                aa [ k++ ] = (CharAssociation) associations.get(i);
            }
        }
        return aa;
    }

    /**
     * Enable or disable predications.
     * @param enable true if predications are to be enabled; otherwise false to disable
     */
    public void setPredications(boolean enable) {
        this.predications = enable;
    }

    /**
     * Obtain predications state.
     * @return true if predications are enabled
     */
    public boolean getPredications() {
        return this.predications;
    }

    /**
     * Set predication &lt;KEY,VALUE&gt; at glyph sequence OFFSET.
     * @param offset offset (index) into glyph sequence
     * @param key predication key
     * @param value predication value
     */
    public void setPredication(int offset, String key, Object value) {
        if (predications) {
            CharAssociation[] aa = getAssociations(offset, 1);
            CharAssociation   ca = aa[0];
            ca.setPredication(key, value);
        }
    }

    /**
     * Get predication KEY at glyph sequence OFFSET.
     * @param offset offset (index) into glyph sequence
     * @param key predication key
     * @return predication KEY at OFFSET or null if none exists
     */
    public Object getPredication(int offset, String key) {
        if (predications) {
            CharAssociation[] aa = getAssociations(offset, 1);
            CharAssociation   ca = aa[0];
            return ca.getPredication(key);
        } else {
            return null;
        }
    }

    /**
     * Compare glyphs.
     * @param gb buffer containing glyph indices with which this glyph sequence's glyphs are to be compared
     * @return zero if glyphs are the same, otherwise returns 1 or -1 according to whether this glyph sequence's
     * glyphs are lexicographically greater or lesser than the glyphs in the specified string buffer
     */
    public int compareGlyphs(IntBuffer gb) {
        int ng = getGlyphCount();
        for (int i = 0, n = gb.limit(); i < n; i++) {
            if (i < ng) {
                int g1 = glyphs.get(i);
                int g2 = gb.get(i);
                if (g1 > g2) {
                    return 1;
                } else if (g1 < g2) {
                    return -1;
                }
            } else {
                return -1;              // this gb is a proper prefix of specified gb
            }
        }
        return 0;                       // same lengths with no difference
    }

    /** {@inheritDoc} */
    public Object clone() {
        try {
            GlyphSequence gs = (GlyphSequence) super.clone();
            gs.characters = copyBuffer(characters);
            gs.glyphs = copyBuffer(glyphs);
            gs.associations = copyAssociations(associations);
            return gs;
        } catch (CloneNotSupportedException e) {
            return null;
        }
    }

    /** {@inheritDoc} */
    public String toString() {
        StringBuffer sb = new StringBuffer();
        sb.append('{');
        sb.append("chars = [");
        sb.append(characters);
        sb.append("], glyphs = [");
        sb.append(glyphs);
        sb.append("], associations = [");
        sb.append(associations);
        sb.append("]");
        sb.append('}');
        return sb.toString();
    }

    /**
     * Determine if two arrays of glyphs are identical.
     * @param ga1 first glyph array
     * @param ga2 second glyph array
     * @return true if arrays are botth null or both non-null and have identical elements
     */
    public static boolean sameGlyphs(int[] ga1, int[] ga2) {
        if (ga1 == ga2) {
            return true;
        } else if ((ga1 == null) || (ga2 == null)) {
            return false;
        } else if (ga1.length != ga2.length) {
            return false;
        } else {
            for (int i = 0, n = ga1.length; i < n; i++) {
                if (ga1[i] != ga2[i]) {
                    return false;
                }
            }
            return true;
        }
    }

    /**
     * Concatenante glyph arrays.
     * @param bga backtrack glyph array
     * @param iga input glyph array
     * @param lga lookahead glyph array
     * @return new integer buffer containing concatenated glyphs
     */
    public static IntBuffer concatGlyphs(int[] bga, int[] iga, int[] lga) {
        int ng = 0;
        if (bga != null) {
            ng += bga.length;
        }
        if (iga != null) {
            ng += iga.length;
        }
        if (lga != null) {
            ng += lga.length;
        }
        IntBuffer gb = IntBuffer.allocate(ng);
        if (bga != null) {
            gb.put(bga);
        }
        if (iga != null) {
            gb.put(iga);
        }
        if (lga != null) {
            gb.put(lga);
        }
        gb.flip();
        return gb;
    }

    /**
     * Concatenante association arrays.
     * @param baa backtrack association array
     * @param iaa input association array
     * @param laa lookahead association array
     * @return new list containing concatenated associations
     */
    public static List concatAssociations(CharAssociation[] baa, CharAssociation[] iaa, CharAssociation[] laa) {
        int na = 0;
        if (baa != null) {
            na += baa.length;
        }
        if (iaa != null) {
            na += iaa.length;
        }
        if (laa != null) {
            na += laa.length;
        }
        if (na > 0) {
            List gl = new ArrayList(na);
            if (baa != null) {
                Collections.addAll(gl, baa);
            }
            if (iaa != null) {
                Collections.addAll(gl, iaa);
            }
            if (laa != null) {
                Collections.addAll(gl, laa);
            }
            return gl;
        } else {
            return null;
        }
    }

    /**
     * Join (concatenate) glyph sequences.
     * @param gs original glyph sequence from which to reuse character array reference
     * @param sa array of glyph sequences, whose glyph arrays and association lists are to be concatenated
     * @return new glyph sequence referring to character array of GS and concatenated glyphs and associations of SA
     */
    public static GlyphSequence join(GlyphSequence gs, GlyphSequence[] sa) {
        assert sa != null;
        int tg = 0;
        int ta = 0;
        for (GlyphSequence s : sa) {
            IntBuffer ga = s.getGlyphs();
            assert ga != null;
            int ng = ga.limit();
            List al = s.getAssociations();
            assert al != null;
            int na = al.size();
            assert na == ng;
            tg += ng;
            ta += na;
        }
        IntBuffer uga = IntBuffer.allocate(tg);
        ArrayList ual = new ArrayList(ta);
        for (GlyphSequence s : sa) {
            uga.put(s.getGlyphs());
            ual.addAll(s.getAssociations());
        }
        return new GlyphSequence(gs.getCharacters(), uga, ual, gs.getPredications());
    }

    /**
     * Reorder sequence such that [SOURCE,SOURCE+COUNT) is moved just prior to TARGET.
     * @param gs input sequence
     * @param source index of sub-sequence to reorder
     * @param count length of sub-sequence to reorder
     * @param target index to which source sub-sequence is to be moved
     * @return reordered sequence (or original if no reordering performed)
     */
    public static GlyphSequence reorder(GlyphSequence gs, int source, int count, int target) {
        if (source != target) {
            int   ng  = gs.getGlyphCount();
            int[] ga  = gs.getGlyphArray(false);
            int[] nga = new int [ ng ];
            CharAssociation[] aa  = gs.getAssociations(0, ng);
            CharAssociation[] naa = new CharAssociation [ ng ];
            if (source < target) {
                int t = 0;
                for (int s = 0, e = source; s < e; s++, t++) {
                    nga[t] = ga[s];
                    naa[t] = aa[s];
                }
                for (int s = source + count, e = target; s < e; s++, t++) {
                    nga[t] = ga[s];
                    naa[t] = aa[s];
                }
                for (int s = source, e = source + count; s < e; s++, t++) {
                    nga[t] = ga[s];
                    naa[t] = aa[s];
                }
                for (int s = target, e = ng; s < e; s++, t++) {
                    nga[t] = ga[s];
                    naa[t] = aa[s];
                }
            } else {
                int t = 0;
                for (int s = 0, e = target; s < e; s++, t++) {
                    nga[t] = ga[s];
                    naa[t] = aa[s];
                }
                for (int s = source, e = source + count; s < e; s++, t++) {
                    nga[t] = ga[s];
                    naa[t] = aa[s];
                }
                for (int s = target, e = source; s < e; s++, t++) {
                    nga[t] = ga[s];
                    naa[t] = aa[s];
                }
                for (int s = source + count, e = ng; s < e; s++, t++) {
                    nga[t] = ga[s];
                    naa[t] = aa[s];
                }
            }
            return new GlyphSequence(gs, null, nga, null, null, naa, null);
        } else {
            return gs;
        }
    }

    private static int[] toArray(IntBuffer ib) {
        if (ib != null) {
            int n = ib.limit();
            int[] ia = new int[n];
            ib.get(ia, 0, n);
            return ia;
        } else {
            return new int[0];
        }
    }

    private static List makeIdentityAssociations(int numChars, int numGlyphs) {
        int nc = numChars;
        int ng = numGlyphs;
        List av = new ArrayList(ng);
        for (int i = 0, n = ng; i < n; i++) {
            int k = (i > nc) ? nc : i;
            av.add(new CharAssociation(i, (k == nc) ? 0 : 1));
        }
        return av;
    }

    private static IntBuffer copyBuffer(IntBuffer ib) {
        if (ib != null) {
            int[] ia = new int [ ib.capacity() ];
            int   p  = ib.position();
            int   l  = ib.limit();
            System.arraycopy(ib.array(), 0, ia, 0, ia.length);
            return IntBuffer.wrap(ia, p, l - p);
        } else {
            return null;
        }
    }

    private static List copyAssociations(List ca) {
        if (ca != null) {
            return new ArrayList(ca);
        } else {
            return ca;
        }
    }

}
