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

package org.apache.commons.imaging.formats.tiff;

import static org.junit.jupiter.api.Assertions.assertEquals;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.apache.commons.imaging.ImageReadException;
import org.apache.commons.imaging.common.bytesource.ByteSource;
import org.apache.commons.imaging.common.bytesource.ByteSourceFile;
import org.apache.commons.imaging.common.mylzw.MyLzwCompressor;
import org.apache.commons.imaging.common.mylzw.MyLzwDecompressor;
import org.apache.commons.imaging.internal.Debug;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;

public class TiffLzwTest extends TiffBaseTest {

    @Test
    public void testTrivial() throws Exception {
        final byte[] bytes = { 0, };
        compressRoundtripAndValidate(bytes);
    }

    @Test
    public void testMedium() throws Exception {
        final int LENGTH = 1024 * 32;
        final byte[] bytes = new byte[LENGTH];
        for (int modulator = 1; modulator < 255; modulator += 3) {
            for (int i = 0; i < LENGTH; i++) {
                bytes[i] = (byte) (0xff & (i % modulator));
            }

            compressRoundtripAndValidate(bytes);
        }
    }

    @Disabled // FIXME fails with java.io.IOException: Bad Code: -1 codes: 258 code_size: 9, table: 4096
    @Test
    public void testTiffImageData() throws IOException, ImageReadException {
        final List<File> images = getTiffImages();
        for (final File image : images) {

            Debug.debug("imageFile", image);

            final ByteSource byteSource = new ByteSourceFile(image);
            final List<byte[]> data = new TiffImageParser().collectRawImageData(byteSource,
                    Collections.emptyMap());

            for (final byte[] bytes : data) {
                decompressRoundtripAndValidate(bytes);
            }
        }
    }

    private void compressRoundtripAndValidate(final byte src[]) throws IOException {
        final boolean DEBUG = false;

        if (DEBUG) {
            Debug.debug();
            Debug.debug("roundtripAndValidate: " + src.length);
            Debug.debug();
        }

        final int LZW_MINIMUM_CODE_SIZE = 8;
        final List<Integer> codes = new ArrayList<>();
        final MyLzwCompressor.Listener compressionListener = new MyLzwCompressor.Listener() {
            @Override
            public void dataCode(final int code) {
                codes.add(code);
            }

            @Override
            public void eoiCode(final int code) {
                codes.add(code);
            }

            @Override
            public void clearCode(final int code) {
                codes.add(code);
            }

            @Override
            public void init(final int clearCode, final int eoiCode) {
            }
        };

        final MyLzwCompressor compressor = new MyLzwCompressor(LZW_MINIMUM_CODE_SIZE,
                ByteOrder.BIG_ENDIAN, true, compressionListener);
        final byte compressed[] = compressor.compress(src);

        final MyLzwDecompressor.Listener decompressionListener = new MyLzwDecompressor.Listener() {

            int index = 0;
            int clearCode, eoiCode;

            @Override
            public void code(final int code) {
                if (DEBUG) {
                    if (code == clearCode) {
                        Debug.debug("clearCode: " + index + "/" + codes.size());
                        Debug.debug();
                    }
                    if (code == eoiCode) {
                        Debug.debug("eoiCode: " + index + "/" + codes.size());
                        Debug.debug();
                    }
                }
                final Integer expectedCode = codes.get(index++);
                if (code != expectedCode) {
                    Debug.debug("bad code: " + index + "/" + codes.size());
                    Debug.debug("code: " + code + " (0x"
                            + Integer.toHexString(code) + ") "
                            + Integer.toBinaryString(code));
                    Debug.debug("expected: " + expectedCode + " (0x"
                            + Integer.toHexString(expectedCode)
                            + ") "
                            + Integer.toBinaryString(expectedCode));
                    Debug.debug("clearCode: " + clearCode + " (0x"
                            + Integer.toHexString(clearCode) + ") "
                            + Integer.toBinaryString(clearCode));
                    Debug.debug("eoiCode: " + eoiCode + " (0x"
                            + Integer.toHexString(eoiCode) + ") "
                            + Integer.toBinaryString(eoiCode));
                    Debug.debug();
                }
            }

            @Override
            public void init(final int clearCode, final int eoiCode) {
                this.clearCode = clearCode;
                this.eoiCode = eoiCode;
            }

        };
        final InputStream is = new ByteArrayInputStream(compressed);
        final MyLzwDecompressor decompressor = new MyLzwDecompressor(
                LZW_MINIMUM_CODE_SIZE, ByteOrder.BIG_ENDIAN,
                decompressionListener);
        decompressor.setTiffLZWMode();
        final byte decompressed[] = decompressor.decompress(is, src.length);

        assertEquals(src.length, decompressed.length);
        for (int i = 0; i < src.length; i++) {
            assertEquals(src[i], decompressed[i]);
        }
    }

    private void decompressRoundtripAndValidate(final byte src[]) throws IOException {
        Debug.debug();
        Debug.debug("roundtripAndValidate: " + src.length);
        Debug.debug();

        final int LZW_MINIMUM_CODE_SIZE = 8;
        final List<Integer> codes = new ArrayList<>();

        final MyLzwDecompressor.Listener decompressionListener = new MyLzwDecompressor.Listener() {

            @Override
            public void code(final int code) {
                Debug.debug("listener code: " + code + " (0x"
                        + Integer.toHexString(code) + ") "
                        + Integer.toBinaryString(code) + ", index: "
                        + codes.size());
                codes.add(code);
            }

            @Override
            public void init(final int clearCode, final int eoiCode) {
            }

        };
        final InputStream is = new ByteArrayInputStream(src);
        final MyLzwDecompressor decompressor = new MyLzwDecompressor(
                LZW_MINIMUM_CODE_SIZE, ByteOrder.BIG_ENDIAN,
                decompressionListener);
        decompressor.setTiffLZWMode();
        final byte decompressed[] = decompressor.decompress(is, src.length);

        final MyLzwCompressor.Listener compressionListener = new MyLzwCompressor.Listener() {

            int clearCode, eoiCode;

            @Override
            public void init(final int clearCode, final int eoiCode) {
                this.clearCode = clearCode;
                this.eoiCode = eoiCode;
            }

            int index = 0;

            private void code(final int code) {

                if (code == clearCode) {
                    Debug.debug("clearCode: " + index + "/" + codes.size());
                    Debug.debug();
                }
                if (code == eoiCode) {
                    Debug.debug("eoiCode: " + index + "/" + codes.size());
                    Debug.debug();
                }
                final Integer expectedCode = codes.get(index++);
                if (code != expectedCode) {
                    Debug.debug("bad code: " + index + "/" + codes.size());
                    Debug.debug("code: " + code + " (0x"
                            + Integer.toHexString(code) + ") "
                            + Integer.toBinaryString(code));
                    Debug.debug("expected: " + expectedCode + " (0x"
                            + Integer.toHexString(expectedCode)
                            + ") "
                            + Integer.toBinaryString(expectedCode));
                    Debug.debug("clearCode: " + clearCode + " (0x"
                            + Integer.toHexString(clearCode) + ") "
                            + Integer.toBinaryString(clearCode));
                    Debug.debug("eoiCode: " + eoiCode + " (0x"
                            + Integer.toHexString(eoiCode) + ") "
                            + Integer.toBinaryString(eoiCode));
                    Debug.debug();
                }
            }

            @Override
            public void dataCode(final int code) {
                code(code);
            }

            @Override
            public void eoiCode(final int code) {
                code(code);
            }

            @Override
            public void clearCode(final int code) {
                code(code);
            }

        };

        final MyLzwCompressor compressor = new MyLzwCompressor(LZW_MINIMUM_CODE_SIZE,
                ByteOrder.BIG_ENDIAN, true, compressionListener);
        final byte compressed[] = compressor.compress(decompressed);

        assertEquals(src.length, compressed.length);
        for (int i = 0; i < src.length; i++) {
            assertEquals(src[i], compressed[i]);
        }
    }

}
