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

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

import org.apache.fop.complexscripts.util.CharAssociation;
import org.apache.fop.complexscripts.util.GlyphSequence;
import org.apache.fop.complexscripts.util.ScriptContextTester;

// CSOFF: LineLengthCheck

/**
 * <p>The <code>GlyphSubstitutionState</code> implements an state object used during glyph substitution
 * processing.</p>
 *
 * <p>This work was originally authored by Glenn Adams (gadams@apache.org).</p>
 */

public class GlyphSubstitutionState extends GlyphProcessingState {

    /** alternates index */
    private int[] alternatesIndex;
    /** current output glyph sequence */
    private IntBuffer ogb;
    /** current output glyph to character associations */
    private List oal;
    /** character association predications */
    private boolean predications;

    /**
     * Construct default (reset) glyph substitution state.
     */
    public GlyphSubstitutionState() {
    }

    /**
     * Construct glyph substitution state.
     * @param gs input glyph sequence
     * @param script script identifier
     * @param language language identifier
     * @param feature feature identifier
     * @param sct script context tester (or null)
     */
    public GlyphSubstitutionState(GlyphSequence gs, String script, String language, String feature, ScriptContextTester sct) {
        super(gs, script, language, feature, sct);
        this.ogb = IntBuffer.allocate(gs.getGlyphCount());
        this.oal = new ArrayList(gs.getGlyphCount());
        this.predications = gs.getPredications();
    }

    /**
     * Construct glyph substitution state using an existing state object using shallow copy
     * except as follows: input glyph sequence is copied deep except for its characters array.
     * @param ss existing positioning state to copy from
     */
    public GlyphSubstitutionState(GlyphSubstitutionState ss) {
        super(ss);
        this.ogb = IntBuffer.allocate(indexLast);
        this.oal = new ArrayList(indexLast);
    }

    /**
     * Reset glyph substitution state.
     * @param gs input glyph sequence
     * @param script script identifier
     * @param language language identifier
     * @param feature feature identifier
     * @param sct script context tester (or null)
     */
    public GlyphSubstitutionState reset(GlyphSequence gs, String script, String language, String feature, ScriptContextTester sct) {
        super.reset(gs, script, language, feature, sct);
        this.alternatesIndex = null;
        this.ogb = IntBuffer.allocate(gs.getGlyphCount());
        this.oal = new ArrayList(gs.getGlyphCount());
        this.predications = gs.getPredications();
        return this;
    }

    /**
     * Set alternates indices.
     * @param alternates array of alternates indices ordered by coverage index
     */
    public void setAlternates(int[] alternates) {
        this.alternatesIndex = alternates;
    }

    /**
     * Obtain alternates index associated with specified coverage index. An alternates
     * index is used to select among stylistic alternates of a glyph at a particular
     * coverage index. This information must be provided by the document itself (in the
     * form of an extension attribute value), since a font has no way to determine which
     * alternate the user desires.
     * @param ci coverage index
     * @return an alternates index
     */
    public int getAlternatesIndex(int ci) {
        if (alternatesIndex == null) {
            return 0;
        } else if ((ci < 0) || (ci > alternatesIndex.length)) {
            return 0;
        } else {
            return alternatesIndex [ ci ];
        }
    }

    /**
     * Put (write) glyph into glyph output buffer.
     * @param glyph to write
     * @param a character association that applies to glyph
     * @param predication a predication value to add to association A if predications enabled
     */
    public void putGlyph(int glyph, CharAssociation a, Object predication) {
        if (!ogb.hasRemaining()) {
            ogb = growBuffer(ogb);
        }
        ogb.put(glyph);
        if (predications && (predication != null)) {
            a.setPredication(feature, predication);
        }
        oal.add(a);
    }

    /**
     * Put (write) array of glyphs into glyph output buffer.
     * @param glyphs to write
     * @param associations array of character associations that apply to glyphs
     * @param predication optional predicaion object to be associated with glyphs' associations
     */
    public void putGlyphs(int[] glyphs, CharAssociation[] associations, Object predication) {
        assert glyphs != null;
        assert associations != null;
        assert associations.length >= glyphs.length;
        for (int i = 0, n = glyphs.length; i < n; i++) {
            putGlyph(glyphs [ i ], associations [ i ], predication);
        }
    }

    /**
     * Obtain output glyph sequence.
     * @return newly constructed glyph sequence comprised of original
     * characters, output glyphs, and output associations
     */
    public GlyphSequence getOutput() {
        int position = ogb.position();
        if (position > 0) {
            ogb.limit(position);
            ogb.rewind();
            return new GlyphSequence(igs.getCharacters(), ogb, oal);
        } else {
            return igs;
        }
    }

    /**
     * Apply substitution subtable to current state at current position (only),
     * resulting in the consumption of zero or more input glyphs, and possibly
     * replacing the current input glyphs starting at the current position, in
     * which case it is possible that indexLast is altered to be either less than
     * or greater than its value prior to this application.
     * @param st the glyph substitution subtable to apply
     * @return true if subtable applied, or false if it did not (e.g., its
     * input coverage table did not match current input context)
     */
    public boolean apply(GlyphSubstitutionSubtable st) {
        assert st != null;
        updateSubtableState(st);
        boolean applied = st.substitute(this);
        return applied;
    }

    /**
     * Apply a sequence of matched rule lookups to the <code>nig</code> input glyphs
     * starting at the current position. If lookups are non-null and non-empty, then
     * all input glyphs specified by <code>nig</code> are consumed irregardless of
     * whether any specified lookup applied.
     * @param lookups array of matched lookups (or null)
     * @param nig number of glyphs in input sequence, starting at current position, to which
     * the lookups are to apply, and to be consumed once the application has finished
     * @return true if lookups are non-null and non-empty; otherwise, false
     */
    public boolean apply(GlyphTable.RuleLookup[] lookups, int nig) {
        // int nbg = index;
        int nlg = indexLast - (index + nig);
        int nog = 0;
        if ((lookups != null) && (lookups.length > 0)) {
            // apply each rule lookup to extracted input glyph array
            for (int i = 0, n = lookups.length; i < n; i++) {
                GlyphTable.RuleLookup l = lookups [ i ];
                if (l != null) {
                    GlyphTable.LookupTable lt = l.getLookup();
                    if (lt != null) {
                        // perform substitution on a copy of previous state
                        GlyphSubstitutionState ss = new GlyphSubstitutionState(this);
                        // apply lookup table substitutions
                        GlyphSequence gs = lt.substitute(ss, l.getSequenceIndex());
                        // replace current input sequence starting at current position with result
                        if (replaceInput(0, -1, gs)) {
                            nog = gs.getGlyphCount() - nlg;
                        }
                    }
                }
            }
            // output glyphs and associations
            putGlyphs(getGlyphs(0, nog, false, null, null, null), getAssociations(0, nog, false, null, null, null), null);
            // consume replaced input glyphs
            consume(nog);
            return true;
        } else {
            return false;
        }
    }

    /**
     * Apply default application semantices; namely, consume one input glyph,
     * writing that glyph (and its association) to the output glyphs (and associations).
     */
    public void applyDefault() {
        super.applyDefault();
        int gi = getGlyph();
        if (gi != 65535) {
            putGlyph(gi, getAssociation(), null);
        }
    }

    private static IntBuffer growBuffer(IntBuffer ib) {
        int capacity = ib.capacity();
        int capacityNew = capacity * 2;
        IntBuffer ibNew = IntBuffer.allocate(capacityNew);
        ib.rewind();
        return ibNew.put(ib);
    }

}
