/*
 * 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: WordArea.java 1862426 2019-07-02 13:19:54Z ssteiner $ */

package org.apache.fop.area.inline;

import java.util.Arrays;
import java.util.List;

import org.apache.fop.complexscripts.bidi.InlineRun;
import org.apache.fop.complexscripts.util.CharMirror;

/**
 * A string of characters without spaces
 */
public class WordArea extends InlineArea {

    private static final long serialVersionUID = 6444644662158970942L;

    /** The text for this word area */
    protected String word;

    /** An array of width for adjusting the individual letters (optional) */
    protected int[] letterAdjust;

    /**
     * An array of resolved bidirectional levels corresponding to each character
     * in word (optional)
     */
    protected int[] levels;

    /**
     * An array of glyph positioning adjustments to apply to each glyph 'char' in word (optional)
     */
    protected int[][] gposAdjustments;

    /**
     * A flag indicating whether the content of word is reversed in relation to
     * its original logical order.
     */
    protected boolean reversed;
    private boolean nextIsSpace;

    /**
     * Create a word area
     * @param blockProgressionOffset the offset for this area
     * @param level the bidirectional embedding level (or -1 if not defined) for word as a group
     * @param word the word string
     * @param letterAdjust the letter adjust array (may be null)
     * @param levels array of per-character (glyph) bidirectional levels,
     * in case word area is heterogenously leveled
     * @param gposAdjustments array of general position adjustments or null if none apply
     * @param reversed true if word is known to be reversed at construction time
     */
    public WordArea(int blockProgressionOffset, int level, String word, int[] letterAdjust, int[] levels,
          int[][] gposAdjustments, boolean reversed, boolean nextIsSpace) {
        super(blockProgressionOffset, level);
        int length = (word != null) ? word.length() : 0;
        this.word = word;
        this.letterAdjust = maybeAdjustLength(letterAdjust, length);
        this.levels = maybePopulateLevels(levels, level, length);
        this.gposAdjustments = maybeAdjustLength(gposAdjustments, length);
        this.reversed = reversed;
        this.nextIsSpace = nextIsSpace;
    }

    public WordArea(int blockProgressionOffset, int level, String word, int[] letterAdjust, int[] levels,
            int[][] gposAdjustments, boolean reversed) {
        this(blockProgressionOffset, level, word, letterAdjust, levels, gposAdjustments, reversed, false);
    }

    /**
     * Create a word area
     * @param blockProgressionOffset the offset for this area
     * @param level the bidirectional embedding level (or -1 if not defined) for word as a group
     * @param word the word string
     * @param letterAdjust the letter adjust array (may be null)
     * @param levels array of per-character (glyph) bidirectional levels,
     * in case word area is heterogenously leveled
     * @param gposAdjustments array of general position adjustments or null if none apply
     */
    public WordArea(
    int blockProgressionOffset, int level, String word, int[] letterAdjust, int[] levels,
      int[][] gposAdjustments) {
        this (blockProgressionOffset, level, word, letterAdjust, levels, gposAdjustments, false);
    }

    /** @return Returns the word. */
    public String getWord() {
        return word;
    }

    /** @return the array of letter adjust widths */
    public int[] getLetterAdjustArray() {
        return this.letterAdjust;
    }

    /**
     * Obtain per-character (glyph) bidi levels.
     * @return a (possibly empty) array of levels or null (if none resolved)
     */
    public int[] getBidiLevels() {
        return levels;
    }

    /**
     * <p>Obtain per-character (glyph) bidi levels over a specified subsequence.</p>
     * <p>If word has been reversed, then the subsequence is over the reversed word.</p>
     * @param start starting (inclusive) index of subsequence
     * @param end ending (exclusive) index of subsequence
     * @return a (possibly null) array of per-character (glyph) levels over the specified
     * sequence
     */
    public int[] getBidiLevels(int start, int end) {
        assert start <= end;
        if (this.levels != null) {
            int n = end - start;
            int[] levels = new int [ n ];
            System.arraycopy(this.levels, start + 0, levels, 0, n);
            return levels;
        } else {
            return null;
        }
    }

    /**
     * <p>Obtain per-character (glyph) level at a specified index position.</p>
     * <p>If word has been reversed, then the position is relative to the reversed word.</p>
     * @param position the index of the (possibly reversed) character from which to obtain the
     * level
     * @return a resolved bidirectional level or, if not specified, then -1
     */
    public int bidiLevelAt(int position) {
        if (position > word.length()) {
            throw new IndexOutOfBoundsException();
        } else if (levels != null) {
            return levels [ position ];
        } else {
            return -1;
        }
    }

    @Override
    public List collectInlineRuns(List runs) {
        assert runs != null;
        InlineRun r;
        int[] levels = getBidiLevels();
        if ((levels != null) && (levels.length > 0)) {
            r = new InlineRun(this, levels);
        } else {
            r = new InlineRun(this, getBidiLevel(), word.length());
        }
        runs.add(r);
        return runs;
    }

    /**
     * Obtain per-character (glyph) position adjustments.
     * @return a (possibly empty) array of adjustments, each having four elements, or null
     * if no adjustments apply
     */
    public int[][] getGlyphPositionAdjustments() {
        return gposAdjustments;
    }

    /**
     * <p>Obtain per-character (glyph) position adjustments at a specified index position.</p>
     * <p>If word has been reversed, then the position is relative to the reversed word.</p>
     * @param position the index of the (possibly reversed) character from which to obtain the
     * level
     * @return an array of adjustments or null if none applies
     */
    public int[] glyphPositionAdjustmentsAt(int position) {
        if (position > word.length()) {
            throw new IndexOutOfBoundsException();
        } else if (gposAdjustments != null) {
            return gposAdjustments [ position ];
        } else {
            return null;
        }
    }

    /**
     * <p>Reverse characters and corresponding per-character levels and glyph position
     * adjustments.</p>
     * @param mirror if true, then perform mirroring if mirrorred characters
     */
    public void reverse(boolean mirror) {
        if (word.length() > 0) {
            word = ((new StringBuffer(word)) .reverse()) .toString();
            if (levels != null) {
                reverse(levels);
            }
            if (gposAdjustments != null) {
                reverse(gposAdjustments);
            }
            reversed = !reversed;
            if (mirror) {
                word = CharMirror.mirror(word);
            }
        }
    }

    /**
     * <p>Perform mirroring on mirrorable characters.</p>
     */
    public void mirror() {
        if (word.length() > 0) {
            word = CharMirror.mirror(word);
        }
    }

    /**
     * <p>Determined if word has been reversed (in relation to original logical order).</p>
     * <p>If a word is reversed, then both its characters (glyphs) and corresponding per-character
     * levels are in reverse order.</p>
     * <p>Note: this information is used in order to process non-spacing marks during rendering as
     * well as provide hints for caret direction.</p>
     * @return true if word is reversed
     */
    public boolean isReversed() {
        return reversed;
    }

    public boolean isNextIsSpace() {
        return nextIsSpace;
    }

    /*
     * If int[] array is not of specified length, then create
     * a new copy of the first length entries.
     */
    private static int[] maybeAdjustLength(int[] ia, int length) {
        if (ia != null) {
            if (ia.length == length) {
                return ia;
            } else {
                int[] iaNew = new int [ length ];
                for (int i = 0, n = ia.length; i < n; i++) {
                    if (i < length) {
                        iaNew [ i ] = ia [ i ];
                    } else {
                        break;
                    }
                }
                return iaNew;
            }
        } else {
            return ia;
        }
    }

    /*
     * If int[][] matrix is not of specified length, then create
     * a new shallow copy of the first length entries.
     */
    private static int[][] maybeAdjustLength(int[][] im, int length) {
        if (im != null) {
            if (im.length == length) {
                return im;
            } else {
                int[][] imNew = new int [ length ][];
                for (int i = 0, n = im.length; i < n; i++) {
                    if (i < length) {
                        imNew [ i ] = im [ i ];
                    } else {
                        break;
                    }
                }
                return imNew;
            }
        } else {
            return im;
        }
    }

    private static int[] maybePopulateLevels(int[] levels, int level, int count) {
        if ((levels == null) && (level >= 0)) {
            levels = new int[count];
            Arrays.fill(levels, level);
        }
        return maybeAdjustLength(levels, count);
    }

    private static void reverse(int[] a) {
        for (int i = 0, n = a.length, m = n / 2; i < m; i++) {
            int k = n - i - 1;
            int t = a [ k ];
            a [ k ] = a [ i ];
            a [ i ] = t;
        }
    }

    private static void reverse(int[][] aa) {
        for (int i = 0, n = aa.length, m = n / 2; i < m; i++) {
            int k = n - i - 1;
            int[] t = aa [ k ];
            aa [ k ] = aa [ i ];
            aa [ i ] = t;
        }
    }

}
