/*
 * 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.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Stack;
import java.util.Vector;

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

import org.apache.fop.area.LineArea;
import org.apache.fop.area.inline.InlineArea;
import org.apache.fop.fo.pagination.PageSequence;

// CSOFF: LineLengthCheck

/**
 * <p>A utility class for performing bidirectional resolution processing.</p>
 *
 * <p>This work was originally authored by Glenn Adams (gadams@apache.org).</p>
 */
public final class BidiResolver {

    /**
     * logging instance
     */
    private static final Log log = LogFactory.getLog(BidiResolver.class);

    private BidiResolver() {
    }

    /**
     * Resolve inline directionality.
     * @param ps a page sequence FO instance
     */
    public static void resolveInlineDirectionality(PageSequence ps) {
        if (log.isDebugEnabled()) {
            log.debug("BD: RESOLVE: " + ps);
        }
        // 1. collect delimited text ranges
        List ranges = ps.collectDelimitedTextRanges(new Stack());
        if (log.isDebugEnabled()) {
            dumpRanges("BD: RESOLVE: RANGES:", ranges);
        }
        // 2. prune empty ranges
        ranges = pruneEmptyRanges(ranges);
        if (log.isDebugEnabled()) {
            dumpRanges("BD: RESOLVE: PRUNED RANGES:", ranges);
        }
        // 3. resolve inline directionaly of unpruned ranges
        resolveInlineDirectionality(ranges);
    }

    /**
     * Reorder line area.
     * @param la a line area instance
     */
    public static void reorder(LineArea la) {

        // 1. collect inline levels
        List runs = collectRuns(la.getInlineAreas(), new Vector());
        if (log.isDebugEnabled()) {
            dumpRuns("BD: REORDER: INPUT:", runs);
        }

        // 2. split heterogeneous inlines
        runs = splitRuns(runs);
        if (log.isDebugEnabled()) {
            dumpRuns("BD: REORDER: SPLIT INLINES:", runs);
        }

        // 3. determine minimum and maximum levels
        int[] mm = computeMinMaxLevel(runs, null);
        if (log.isDebugEnabled()) {
            log.debug("BD: REORDER: { min = " + mm[0] + ", max = " + mm[1] + "}");
        }

        // 4. reorder from maximum level to minimum odd level
        int mn = mm[0];
        int mx = mm[1];
        if (mx > 0) {
            for (int l1 = mx, l2 = ((mn & 1) == 0) ? (mn + 1) : mn; l1 >= l2; l1--) {
                runs = reorderRuns(runs, l1);
            }
        }
        if (log.isDebugEnabled()) {
            dumpRuns("BD: REORDER: REORDERED RUNS:", runs);
        }

        // 5. reverse word consituents (characters and glyphs) while mirroring
        boolean mirror = true;
        reverseWords(runs, mirror);
        if (log.isDebugEnabled()) {
            dumpRuns("BD: REORDER: REORDERED WORDS:", runs);
        }

        // 6. replace line area's inline areas with reordered runs' inline areas
        replaceInlines(la, replicateSplitWords(runs));
    }

    private static void resolveInlineDirectionality(List ranges) {
        for (Iterator it = ranges.iterator(); it.hasNext(); ) {
            DelimitedTextRange r = (DelimitedTextRange) it.next();
            r.resolve();
            if (log.isDebugEnabled()) {
                log.debug(r);
            }
        }
    }

    private static List collectRuns(List inlines, List runs) {
        for (Iterator it = inlines.iterator(); it.hasNext(); ) {
            InlineArea ia = (InlineArea) it.next();
            runs = ia.collectInlineRuns(runs);
        }
        return runs;
    }

    private static List splitRuns(List runs) {
        List runsNew = new Vector();
        for (Iterator it = runs.iterator(); it.hasNext(); ) {
            InlineRun ir = (InlineRun) it.next();
            if (ir.isHomogenous()) {
                runsNew.add(ir);
            } else {
                runsNew.addAll(ir.split());
            }
        }
        if (!runsNew.equals(runs)) {
            runs = runsNew;
        }
        return runs;
    }

    private static int[] computeMinMaxLevel(List runs, int[] mm) {
        if (mm == null) {
            mm = new int[] {Integer.MAX_VALUE, Integer.MIN_VALUE};
        }
        for (Iterator it = runs.iterator(); it.hasNext(); ) {
            InlineRun ir = (InlineRun) it.next();
            ir.updateMinMax(mm);
        }
        return mm;
    }
    private static List reorderRuns(List runs, int level) {
        assert level >= 0;
        List runsNew = new Vector();
        for (int i = 0, n = runs.size(); i < n; i++) {
            InlineRun iri = (InlineRun) runs.get(i);
            if (iri.getMinLevel() < level) {
                runsNew.add(iri);
            } else {
                int s = i;
                int e = s;
                while (e < n) {
                    InlineRun ire = (InlineRun) runs.get(e);
                    if (ire.getMinLevel() < level) {
                        break;
                    } else {
                        e++;
                    }
                }
                if (s < e) {
                    runsNew.addAll(reverseRuns(runs, s, e));
                }
                i = e - 1;
            }
        }
        if (!runsNew.equals(runs)) {
            runs = runsNew;
        }
        return runs;
    }
    private static List reverseRuns(List runs, int s, int e) {
        int n = e - s;
        Vector runsNew = new Vector(n);
        if (n > 0) {
            for (int i = 0; i < n; i++) {
                int k = (n - i - 1);
                InlineRun ir = (InlineRun) runs.get(s + k);
                ir.reverse();
                runsNew.add(ir);
            }
        }
        return runsNew;
    }
    private static void reverseWords(List runs, boolean mirror) {
        for (Iterator it = runs.iterator(); it.hasNext(); ) {
            InlineRun ir = (InlineRun) it.next();
            ir.maybeReverseWord(mirror);
        }
    }
    private static List replicateSplitWords(List runs) {
        // [TBD] for each run which inline word area appears multiple times in
        // runs, replicate that word
        return runs;
    }
    private static void replaceInlines(LineArea la, List runs) {
        List<InlineArea> inlines = new ArrayList<InlineArea>();
        for (Iterator it = runs.iterator(); it.hasNext(); ) {
            InlineRun ir = (InlineRun) it.next();
            inlines.add(ir.getInline());
        }
        la.setInlineAreas(unflattenInlines(inlines));
    }
    private static List unflattenInlines(List<InlineArea> inlines) {
        return new UnflattenProcessor(inlines) .unflatten();
    }
    private static void dumpRuns(String header, List runs) {
        log.debug(header);
        for (Iterator it = runs.iterator(); it.hasNext(); ) {
            InlineRun ir = (InlineRun) it.next();
            log.debug(ir);
        }
    }
    private static void dumpRanges(String header, List ranges) {
        log.debug(header);
        for (Iterator it = ranges.iterator(); it.hasNext(); ) {
            DelimitedTextRange r = (DelimitedTextRange) it.next();
            log.debug(r);
        }
    }
    private static List pruneEmptyRanges(List ranges) {
        Vector rv = new Vector();
        for (Iterator it = ranges.iterator(); it.hasNext(); ) {
            DelimitedTextRange r = (DelimitedTextRange) it.next();
            if (!r.isEmpty()) {
                rv.add(r);
            }
        }
        return rv;
    }

}
