/*
 * 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 org.junit.Test;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;

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

/**
 * <p>Test case for Unicode Bidi Algorithm.</p>
 */
public class BidiAlgorithmTestCase {

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

    /**
     * Concatenated array of <test-set,test-sequence> tuples
     * specifying which sequences are to be excluded from testing,
     * where -1 for either component is a wildcard.
     */
    private static final int[] EXCLUSIONS = {
        // no exclusions
    };

    /**
     * Concatenated array of <test-set,test-sequence> tuples
     * specifying which sequences are to be included in testing, where
     * -1 for either component is a wildcard.
     */
    private static final int[] INCLUSIONS = {
        -1, -1                                  // all sequences
    };

    /**
     * Concatenated array of <start,end> tuples expressing ranges of
     * test sets to be tested, where -1 in the end position signifies
     * all remaining test sets.
     */
    private static final int[] TEST_SET_RANGES = {
        0, -1                                   // all test sets
    };

    // instrumentation
    private int includedSequences;
    private int excludedSequences;
    private int passedSequences;

    @Test
    public void testBidiAlgorithm() throws Exception {
        String ldPfx = BidiTestData.LD_PFX;
        int ldCount = BidiTestData.LD_CNT;
        for (int i = 0; i < ldCount; i++) {
            int[] da = BidiTestData.readTestData(ldPfx, i);
            if (da != null) {
                testBidiAlgorithm(i, da);
            } else {
                fail("unable to read bidi test data for resource at index " + i);
            }
        }
        // ensure we passed all test sequences
        assertEquals("did not pass all test sequences", BidiTestData.NUM_TEST_SEQUENCES, passedSequences);
        if (log.isDebugEnabled()) {
            log.debug("Included Sequences : " + includedSequences);
            log.debug("Excluded Sequences : " + excludedSequences);
            log.debug("Passed Sequences   : " + passedSequences);
        }
    }

    private void testBidiAlgorithm(int testSet, int[] da) throws Exception {
        if (da.length < 1) {
            fail("test data is empty");
        } else if (da.length < ((da[0] * 2) + 1)) {
            fail("test data is truncated");
        } else {
            int k = 0;
            // extract level count
            int n = da[k++];
            // extract level array
            int[] la = new int [ n ];
            for (int i = 0; i < n; i++) {
                la[i] = da[k++];
            }
            // extract reorder array
            int[] ra = new int [ n ];
            for (int i = 0; i < n; i++) {
                ra[i] = da[k++];
            }
            // extract and test each test sequence
            int testSequence = 0;
            int[] ta = new int [ n ];
            while ((k + (1 + n)) <= da.length) {
                int bs = da[k++];
                for (int i = 0; i < n; i++) {
                    ta[i] = da[k++];
                }
                if (includeSequence(testSet, testSequence)) {
                    includedSequences++;
                    if (!excludeSequence(testSet, testSequence)) {
                        if (testBidiAlgorithm(testSet, testSequence, la, ra, ta, bs)) {
                            passedSequences++;
                        }
                    } else {
                        excludedSequences++;
                    }
                }
                testSequence++;
            }
            // ensure we exhausted test data
            assertEquals("extraneous test data", da.length, k);
        }
    }

    private boolean includeTestSet(int testSet) {
        for (int i = 0, n = TEST_SET_RANGES.length / 2; i < n; i++) {
            int s = TEST_SET_RANGES [ (i * 2) + 0 ];
            int e = TEST_SET_RANGES [ (i * 2) + 1 ];
            if (testSet >= s) {
                if ((e < 0) || (testSet <= e)) {
                    return true;
                }
            }
        }
        return false;
    }

    private boolean includeSequence(int testSet, int testSequence) {
        if (!includeTestSet(testSet)) {
            return false;
        } else {
            for (int i = 0, n = INCLUSIONS.length / 2; i < n; i++) {
                int setno = INCLUSIONS [ (i * 2) + 0 ];
                int seqno = INCLUSIONS [ (i * 2) + 1 ];
                if (setno < 0) {
                    if (seqno < 0) {
                        return true;
                    } else if (seqno == testSequence) {
                        return true;
                    }
                } else if (setno == testSet) {
                    if (seqno < 0) {
                        return true;
                    } else if (seqno == testSequence) {
                        return true;
                    }
                }
            }
            return false;
        }
    }

    private boolean excludeSequence(int testSet, int testSequence) {
        for (int i = 0, n = EXCLUSIONS.length / 2; i < n; i++) {
            int setno = EXCLUSIONS [ (i * 2) + 0 ];
            int seqno = EXCLUSIONS [ (i * 2) + 1 ];
            if (setno < 0) {
                if (seqno < 0) {
                    return true;
                } else if (seqno == testSequence) {
                    return true;
                }
            } else if (setno == testSet) {
                if (seqno < 0) {
                    return true;
                } else if (seqno == testSequence) {
                    return true;
                }
            }
        }
        return false;
    }

    private boolean testBidiAlgorithm(int testSet, int testSequence, int[] la, int[] ra, int[] ta, int bs)
            throws Exception {
        boolean passed = true;
        int n = la.length;
        if (ra.length != n) {
            fail("bad reorder array length, expected " + n + ", got " + ra.length);
        } else if (ta.length != n) {
            fail("bad test array length, expected " + n + ", got " + ta.length);
        } else {
            // auto-LTR
            if ((bs & 1) != 0) {
                // auto-LTR is performed at higher level
            }
            // LTR
            if ((bs & 2) != 0) {
                int[] levels = UnicodeBidiAlgorithm.resolveLevels(null, ta, 0, new int [ n ], true);
                if (!verifyResults(la, levels, ta, 0, testSet, testSequence)) {
                    passed = false;
                }
            }
            // RTL
            if ((bs & 4) != 0) {
                int[] levels = UnicodeBidiAlgorithm.resolveLevels(null, ta, 1, new int [ n ], true);
                if (!verifyResults(la, levels, ta, 1, testSet, testSequence)) {
                    passed = false;
                }
            }
        }
        return passed;
    }

    private boolean verifyResults(int[] laExp, int[] laOut, int[] ta, int dl, int testSet, int testSequence) {
        if (laOut.length != laExp.length) {
            fail("output levels array length mismatch, expected " + laExp.length + ", got " + laOut.length);
            return false;
        } else {
            int numMatch = 0;
            for (int i = 0, n = laExp.length; i < n; i++) {
                if (laExp[i] >= 0) {
                    int lo = laOut[i];
                    int le = laExp[i];
                    if (lo != le) {
                        assertEquals(getMismatchMessage(testSet, testSequence, i, dl), le, lo);
                    } else {
                        numMatch++;
                    }
                } else {
                    numMatch++;
                }
            }
            return numMatch == laExp.length;
        }
    }

    private String getMismatchMessage(int testSet, int testSequence, int seqIndex, int defaultLevel) {
        StringBuffer sb = new StringBuffer();
        sb.append("level mismatch for default level ");
        sb.append(defaultLevel);
        sb.append(" at sequence index ");
        sb.append(seqIndex);
        sb.append(" in test sequence ");
        sb.append(testSequence);
        sb.append(" of test set ");
        sb.append(testSet);
        return sb.toString();
    }

}
