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

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.EOFException;
import java.io.InputStream;
import java.util.Arrays;

import org.junit.Test;

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

/**
 * MODCAParser and MODCAParser.UnparsedStructuredField Unit tests
 */
public class MODCAParserTestCase {

    /** The carriage control character (0x5A) used to indicate the start of a structured field. */
    public static final byte CARRIAGE_CONTROL_CHAR = (byte)0x5A;
    /**ASCII carriage return control character*/
    public static final byte CARRIAGE_RETURN = (byte)0x0A;
    /**ASCII line feed control character */
    public static final byte LINE_FEED = (byte)0x0D;
    /** 8 byte introducer describe the SF */
    private static final int INTRODUCER_LENGTH = 8;

    /**
     * Test that the MODCA parser recognises carriage control (0x5A) as the Structured Field
     * delimeter
     *
     * @throws Exception *
     */
    @Test
    public void testReadNextStructuredField1() throws Exception {

        // carriage control (0x5A) delimits structured fields,
        // and control is handed to readStructuredField(DataInputStream)
        byte[][] goodInputStream = new byte[][]{
                new byte[]{CARRIAGE_CONTROL_CHAR}
        };

        for (byte[] b : goodInputStream) {
            try {
                new MODCAParser(new ByteArrayInputStream(b))
                        .readNextStructuredField();
                fail("BAD SF should throw EOF: " + byteArrayToString(b));
            } catch (EOFException eof) {
                //passed
            }
        }

        // EOFException thrown when reading the input stream are caught and
        // a null value is returned
        byte[][] badInputStream = new byte[][]{
                new byte[]{},
                new byte[]{CARRIAGE_RETURN},
                new byte[]{LINE_FEED}
        };

        for (byte[] b : badInputStream) {
            UnparsedStructuredField usf = new MODCAParser(new ByteArrayInputStream(b))
            .readNextStructuredField();
            assertNull(usf);
        }
    }


    /**
     * Test that the MODCA parser correctly constructs an UnparsedStructuredField
     * from a well formed structured field
     *
     * @throws Exception *
     */
    @Test
    public void testReadNextStructuredField2() throws Exception {

        // no extension data
        testSF((byte)0xd3, (byte)0xa8, (byte)0x89, //SFTypeID
                (byte)0, //flags excluding the bits for
                //extension present, segmented data and padding present
                false, false,
                new byte[]{0, 0},
                new byte[]{1}, null);

        // with extension data
        testSF((byte)0xd3, (byte)0xa8, (byte)0x89, //SFTypeID
                (byte)0, //flags excluding the bits for
                //extension present, segmented data and padding present
                false, false,
                new byte[]{0, 0},
                new byte[]{1}, new byte[]{10});

        // with ignored reserved bits
        testSF((byte)0xd3, (byte)0xa8, (byte)0x89, //SFTypeID
                (byte)0, //flags excluding the bits for
                //extension present, segmented data and padding present
                false, false,
                new byte[]{1, 2},
                new byte[]{1}, null);

        // with padding present and segmented data
        testSF((byte)0xd3, (byte)0xa8, (byte)0x89, //SFTypeID
                (byte)(1 << 3), //flags excluding the bits for
                //extension present, segmented data and padding present
                true, true,
                new byte[]{0, 0},
                new byte[]{1}, null);

     // with flags non zero
        testSF((byte)0xd3, (byte)0xa8, (byte)0x89, //SFTypeID
                (byte)(1 << 3), //flags excluding the bits for
                //extension present, segmented data and padding present
                false, false,
                new byte[]{0, 0},
                new byte[]{1}, null);
    }


    private void testSF(byte classCode, byte typeCode, byte categoryCode,
            byte flags, boolean segmentedData, boolean paddingPresent, byte[] reserved,
            byte[] data, byte[] extData) throws Exception {

        byte extDataLength = 0;
        boolean extensionPresent = (extData != null);

        if (extensionPresent) {
            flags = (byte)(flags | 0x01);
            extDataLength = (byte)(extData.length + 1); //length includes length byte
        }

        if (segmentedData) {
            flags = (byte)(flags | 0x04);
        }

        if (paddingPresent) {
            flags = (byte)(flags | 0x10);
        }

        short length = (short)(INTRODUCER_LENGTH + data.length + extDataLength);
        byte[] lengthBytes = new byte[]{(byte)(length >> 8), (byte)(length & 0xFF)};

        byte[] sfBytes = new byte[length];

        //introducer bytes
        sfBytes[0] = lengthBytes[0];
        sfBytes[1] = lengthBytes[1];
        sfBytes[2] = classCode;
        sfBytes[3] = typeCode;
        sfBytes[4] = categoryCode;
        sfBytes[5] = flags;
        sfBytes[6] = reserved[0];
        sfBytes[7] = reserved[1];

        if (extDataLength > 0) {
            sfBytes[8] = (byte)(extData.length + 1);
            System.arraycopy(extData, 0, sfBytes, 9, extData.length);
        }

        System.arraycopy(data, 0, sfBytes, length - data.length, data.length);


        byte[] delimiteredSF = new byte[length + 1];

        delimiteredSF[0] = (byte)0x5A;

        System.arraycopy(sfBytes, 0, delimiteredSF, 1, length);

        InputStream bis = new ByteArrayInputStream(delimiteredSF);

        UnparsedStructuredField actual =  new MODCAParser(bis)
        .readNextStructuredField();

        //check introducer
        assertEquals(length, actual.getSfLength());
        assertEquals(classCode, actual.getSfClassCode());
        assertEquals(typeCode, actual.getSfTypeCode());
        assertEquals(categoryCode, actual.getSfCategoryCode());
        assertEquals(extensionPresent, actual.isSfiExtensionPresent());
        assertEquals(segmentedData, actual.isSfiSegmentedData());
        assertEquals(paddingPresent, actual.isSfiPaddingPresent());

        byte[] introducerData = new byte[]{(byte)(length >> 8), (byte)(length & 0xFF),
                classCode,  typeCode, categoryCode, flags, reserved[0], reserved[1]};

        assertTrue(Arrays.equals(introducerData, actual.getIntroducerData()));

        //check data
        assertTrue(Arrays.equals(data, actual.getData()));

        //check extension data
        if (extData != null) {
            assertTrue(Arrays.equals(extData, actual.getExtData()));
        }
        assertEquals(
                (extData == null) ? 0 : extData.length + 1, // 1 byte for length byte
                        actual.getExtLength());

        assertTrue(Arrays.equals(data, actual.getData()));

        int expectedSfTypeID = ((classCode & 0xFF) << 16)
                | ((typeCode & 0xFF) << 8)
                | (categoryCode & 0xFF);

        assertEquals(expectedSfTypeID, actual.getSfTypeID());

        assertTrue(Arrays.equals(sfBytes, actual.getCompleteFieldAsBytes()));

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        actual.writeTo(baos);
        assertTrue(Arrays.equals(sfBytes, baos.toByteArray()));

    }


    private static String byteArrayToString(byte[] byteArray) {
        StringBuilder sb = new StringBuilder();
        for (byte b : byteArray) {
            sb.append(Integer.toHexString(b & 0xFF)).append(" ");
        }
        return sb.toString();
    }

}
