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

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

import org.apache.fop.area.inline.Anchor;
import org.apache.fop.area.inline.InlineArea;
import org.apache.fop.area.inline.InlineBlockParent;
import org.apache.fop.area.inline.InlineParent;
import org.apache.fop.area.inline.InlineViewport;
import org.apache.fop.area.inline.Leader;
import org.apache.fop.area.inline.Space;
import org.apache.fop.area.inline.SpaceArea;
import org.apache.fop.area.inline.UnresolvedPageNumber;
import org.apache.fop.area.inline.WordArea;
import org.apache.fop.util.CharUtilities;

/**
 * The <code>InlineRun</code> class is a utility class, the instances of which are used
 * to capture a sequence of reordering levels associated with an inline area.
 *
 * <p>This work was originally authored by Glenn Adams (gadams@apache.org).</p>
 */
public class InlineRun {
    private InlineArea inline;
    private int[] levels;
    private int minLevel;
    private int maxLevel;
    private int reversals;
    /**
     * Primary constructor.
     * @param inline which generated this inline run
     * @param levels levels array
     */
    public InlineRun(InlineArea inline, int[] levels) {
        assert inline != null;
        assert levels != null;
        this.inline = inline;
        this.levels = levels;
        setMinMax(levels);
    }
    /**
     * Alternate constructor.
     * @param inline which generated this inline run
     * @param level for each index
     * @param count of indices
     */
    public InlineRun(InlineArea inline, int level, int count) {
        this (inline, makeLevels(level, count));
    }
    /**
     * Obtain inline area that generated this inline run.
     * @return inline area that generated this inline run.
     */
    public InlineArea getInline() {
        return inline;
    }
    /**
     * Obtain minimum bidi level for this run.
     * @return minimum bidi level
     */
    public int getMinLevel() {
        return minLevel;
    }
    /**
     * Obtain maximum bidi level for this run.
     * @return maximum bidi level
     */
    public int getMaxLevel() {
        return maxLevel;
    }
    private void setMinMax(int[] levels) {
        int mn = Integer.MAX_VALUE;
        int mx = Integer.MIN_VALUE;
        if ((levels != null) && (levels.length > 0)) {
            for (int l : levels) {
                if (l < mn) {
                    mn = l;
                }
                if (l > mx) {
                    mx = l;
                }
            }
        } else {
            mn = mx = -1;
        }
        this.minLevel = mn;
        this.maxLevel = mx;
    }
    /**
     * Determine if this run has homogenous (same valued) bidi levels.
     * @return true if homogenous
     */
    public boolean isHomogenous() {
        return minLevel == maxLevel;
    }
    /**
     * Split this inline run into homogenous runs.
     * @return list of new runs
     */
    public List split() {
        List runs = new Vector();
        for (int i = 0, n = levels.length; i < n; ) {
            int l = levels [ i ];
            int s = i;
            int e = s;
            while (e < n) {
                if (levels [ e ] != l) {
                    break;
                } else {
                    e++;
                }
            }
            if (s < e) {
                runs.add(new InlineRun(inline, l, e - s));
            }
            i = e;
        }
        assert runs.size() < 2 : "heterogeneous inlines not yet supported!!";
        return runs;
    }
    /**
     * Update a min/max array to correspond with this run's min/max values.
     * @param mm reference to min/max array
     */
    public void updateMinMax(int[] mm) {
        if (minLevel < mm[0]) {
            mm[0] = minLevel;
        }
        if (maxLevel > mm[1]) {
            mm[1] = maxLevel;
        }
    }
    /**
     * Determine if run needs mirroring.
     * @return true if run is homogenous and (positive) odd (i.e., right to left)
     */
    public boolean maybeNeedsMirroring() {
        return (minLevel == maxLevel) && (minLevel > 0) && ((minLevel & 1) != 0);
    }
    /**
     * Reverse run (by incrementing reversal count, not actually reversing).
     */
    public void reverse() {
        reversals++;
    }
    /**
     * Reverse inline area if it is a word area and it requires
     * reversal.
     * @param mirror if true then also mirror characters
     */
    public void maybeReverseWord(boolean mirror) {
        if (inline instanceof WordArea) {
            WordArea w = (WordArea) inline;
            // if not already reversed, then reverse now
            if (!w.isReversed()) {
                if ((reversals & 1) != 0) {
                    w.reverse(mirror);
                } else if (mirror && maybeNeedsMirroring()) {
                    w.mirror();
                }
            }
        }
    }
    @Override
    public boolean equals(Object o) {
        if (o instanceof InlineRun) {
            InlineRun ir = (InlineRun) o;
            if (ir.inline != inline) {
                return false;
            } else if (ir.minLevel != minLevel) {
                return false;
            } else if (ir.maxLevel != maxLevel) {
                return false;
            } else if ((ir.levels != null) && (levels != null)) {
                if (ir.levels.length != levels.length) {
                    return false;
                } else {
                    for (int i = 0, n = levels.length; i < n; i++) {
                        if (ir.levels[i] != levels[i]) {
                            return false;
                        }
                    }
                    return true;
                }
            } else {
                return (ir.levels == null) && (levels == null);
            }
        } else {
            return false;
        }
    }
    @Override
    public int hashCode() {
        int l = (inline != null) ? inline.hashCode() : 0;
        l = (l ^ minLevel) + (l << 19);
        l = (l ^ maxLevel)   + (l << 11);
        return l;
    }
    @Override
    public String toString() {
        StringBuffer sb = new StringBuffer("RR: { type = \'");
        char c;
        String content = null;
        if (inline instanceof WordArea) {
            c = 'W';
            content = ((WordArea) inline) .getWord();
        } else if (inline instanceof SpaceArea) {
            c = 'S';
            content = ((SpaceArea) inline) .getSpace();
        } else if (inline instanceof Anchor) {
            c = 'A';
        } else if (inline instanceof Leader) {
            c = 'L';
        } else if (inline instanceof Space) {
            c = 'S';
        } else if (inline instanceof UnresolvedPageNumber) {
            c = '#';
            content = ((UnresolvedPageNumber) inline) .getText();
        } else if (inline instanceof InlineBlockParent) {
            c = 'B';
        } else if (inline instanceof InlineViewport) {
            c = 'V';
        } else if (inline instanceof InlineParent) {
            c = 'I';
        } else {
            c = '?';
        }
        sb.append(c);
        sb.append("\', levels = \'");
        sb.append(generateLevels(levels));
        sb.append("\', min = ");
        sb.append(minLevel);
        sb.append(", max = ");
        sb.append(maxLevel);
        sb.append(", reversals = ");
        sb.append(reversals);
        sb.append(", content = <");
        sb.append(CharUtilities.toNCRefs(content));
        sb.append("> }");
        return sb.toString();
    }
    private String generateLevels(int[] levels) {
        StringBuffer lb = new StringBuffer();
        int maxLevel = -1;
        int numLevels = levels.length;
        for (int l : levels) {
            if (l > maxLevel) {
                maxLevel = l;
            }
        }
        if (maxLevel < 0) {
            // leave level buffer empty
        } else if (maxLevel < 10) {
            // use string of decimal digits
            for (int level : levels) {
                lb.append((char) ('0' + level));
            }
        } else {
            // use comma separated list
            boolean first = true;
            for (int level : levels) {
                if (first) {
                    first = false;
                } else {
                    lb.append(',');
                }
                lb.append(level);
            }
        }
        return lb.toString();
    }
    private static int[] makeLevels(int level, int count) {
        int[] levels = new int [ count > 0 ? count : 1 ];
        Arrays.fill(levels, level);
        return levels;
    }
}

