/*
 * 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.tika.parser.microsoft.chm;

import java.math.BigInteger;
import java.util.Arrays;

import org.apache.tika.exception.TikaException;

/**
 * LZXC reset table For ensuring a decompression. Reads the block named
 * "::DataSpace/Storage/<SectionName>/Transform/{7FC28940-9D31-11D0-9B27-00A0C91E9C7C}/InstanceData/ResetTable"
 * .
 */
public class ChmLzxcResetTable implements ChmAccessor<ChmLzxcResetTable> {
    private static final long serialVersionUID = -8209574429411707460L;
    /* class members */
    private long version; // 0000: DWORD 2 unknown (possibly a version number)
    private long block_count; // 0004: DWORD Number of entries in reset table
    private long unknown; // 0008: DWORD 8 unknown
    private long table_offset; // 000C: DWORD $28 Length of table header (area
                               // before table entries)
    private long uncompressed_len; // 0010: QWORD Uncompressed Length
    private long compressed_len; // 0018: QWORD Compressed Length
    private long block_len; // 0020: QWORD 0x8000 block size for locations below
    private long[] block_address;

    /* local usage */
    private int dataRemained;
    private int currentPlace = 0;

    private int getDataRemained() {
        return dataRemained;
    }

    private void setDataRemained(int dataRemained) {
        this.dataRemained = dataRemained;
    }

    /**
     * Returns block addresses
     * 
     * @return block addresses
     */
    public long[] getBlockAddress() {
        return block_address;
    }

    /**
     * Sets block addresses
     * 
     * @param block_address
     */
    public void setBlockAddress(long[] block_address) {
        this.block_address = block_address;
    }

    private int getCurrentPlace() {
        return currentPlace;
    }

    private void setCurrentPlace(int currentPlace) {
        this.currentPlace = currentPlace;
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append("version:=" + getVersion()
                + System.getProperty("line.separator"));
        sb.append("block_count:=" + getBlockCount()
                + System.getProperty("line.separator"));
        sb.append("unknown:=" + getUnknown()
                + System.getProperty("line.separator"));
        sb.append("table_offset:=" + getTableOffset()
                + System.getProperty("line.separator"));
        sb.append("uncompressed_len:=" + getUncompressedLen()
                + System.getProperty("line.separator"));
        sb.append("compressed_len:=" + getCompressedLen()
                + System.getProperty("line.separator"));
        sb.append("block_len:=" + getBlockLen()
                + System.getProperty("line.separator"));
        sb.append("block_addresses:=" + Arrays.toString(getBlockAddress()));
        return sb.toString();
    }

    /**
     * Enumerates chm block addresses
     * 
     * @param data
     * 
     * @return byte[] of addresses
     * @throws TikaException 
     */
    private long[] enumerateBlockAddresses(byte[] data) throws TikaException {
        ChmAssert.assertByteArrayNotNull(data);
        /* we have limit of number of blocks to be extracted */
        if (getBlockCount() > 5000)
            setBlockCount(5000);

        if (getBlockCount() < 0 && (getDataRemained() / 8) > 0)
            setBlockCount(getDataRemained() / 8);

        long[] addresses = new long[(int) getBlockCount()];
        int rem = getDataRemained() / 8;
        for (int i = 0; i < rem; i++) {
            long num = -1;

            try {
                addresses[i] = unmarshalUint64(data, num);
            } catch (Exception e) {
                throw new TikaException(e.getMessage());
            }
        }
        return addresses;
    }

    /**
     * Validates parameters such as byte[] and chm lzxc reset table
     * 
     * @param data
     * @param chmLzxcResetTable
     * 
     * @return boolean
     * @throws TikaException 
     */
    private boolean validateParamaters(byte[] data,
            ChmLzxcResetTable chmLzxcResetTable) throws TikaException {
        int goodParameter = 0;
        ChmAssert.assertByteArrayNotNull(data);
        ++goodParameter;
        ChmAssert.assertChmAccessorNotNull(chmLzxcResetTable);
        ++goodParameter;
        return (goodParameter == 2);
    }

    private long unmarshalUInt32(byte[] data, long dest) throws TikaException {
        ChmAssert.assertByteArrayNotNull(data);
        dest = (data[this.getCurrentPlace()] & 0xff)
                | (data[this.getCurrentPlace() + 1] & 0xff) << 8
                | (data[this.getCurrentPlace() + 2] & 0xff) << 16
                | (data[this.getCurrentPlace() + 3] & 0xff) << 24;

        setDataRemained(this.getDataRemained() - 4);
        this.setCurrentPlace(this.getCurrentPlace() + 4);
        return dest;
    }

    private long unmarshalUint64(byte[] data, long dest) throws TikaException {
        ChmAssert.assertByteArrayNotNull(data);
        byte[] temp = new byte[8];
        int i, j;// counters

        for (i = 8, j = 7; i > 0; i--) {
            if (data.length > this.getCurrentPlace()) {
                temp[j--] = data[this.getCurrentPlace()];
                this.setCurrentPlace(this.getCurrentPlace() + 1);
            } else
                throw new TikaException("data is too small to calculate address block");
        }
        dest = new BigInteger(temp).longValue();
        this.setDataRemained(this.getDataRemained() - 8);
        return dest;
    }

    /**
     * Returns the version
     * 
     * @return - long
     */
    public long getVersion() {
        return version;
    }

    /**
     * Sets the version
     * 
     * @param version
     *            - long
     */
    public void setVersion(long version) {
        this.version = version;
    }

    /**
     * Gets a block count
     * 
     * @return - int
     */
    public long getBlockCount() {
        return block_count;
    }

    /**
     * Sets a block count
     * 
     * @param block_count
     *            - long
     */
    public void setBlockCount(long block_count) {
        this.block_count = block_count;
    }

    /**
     * Gets unknown
     * 
     * @return - long
     */
    public long getUnknown() {
        return unknown;
    }

    /**
     * Sets an unknown
     * 
     * @param unknown
     *            - long
     */
    public void setUnknown(long unknown) {
        this.unknown = unknown;
    }

    /**
     * Gets a table offset
     * 
     * @return - long
     */
    public long getTableOffset() {
        return table_offset;
    }

    /**
     * Sets a table offset
     * 
     * @param table_offset
     *            - long
     */
    public void setTableOffset(long table_offset) {
        this.table_offset = table_offset;
    }

    /**
     * Gets uncompressed length
     * 
     * @return - {@link BigInteger }
     */
    public long getUncompressedLen() {
        return uncompressed_len;
    }

    /**
     * Sets uncompressed length
     * 
     * @param uncompressed_len
     *            - {@link BigInteger}
     */
    public void setUncompressedLen(long uncompressed_len) {
        this.uncompressed_len = uncompressed_len;
    }

    /**
     * Gets compressed length
     * 
     * @return - {@link BigInteger}
     */
    public long getCompressedLen() {
        return compressed_len;
    }

    /**
     * Sets compressed length
     * 
     * @param compressed_len
     *            - {@link BigInteger}
     */
    public void setCompressedLen(long compressed_len) {
        this.compressed_len = compressed_len;
    }

    /**
     * Gets a block length
     * 
     * @return - {@link BigInteger}
     */
    public long getBlockLen() {
        return block_len;
    }

    /**
     * Sets a block length
     * 
     * @param block_len
     *            - {@link BigInteger}
     */
    public void setBlockLlen(long block_len) {
        this.block_len = block_len;
    }

    // @Override
    public void parse(byte[] data, ChmLzxcResetTable chmLzxcResetTable) throws TikaException {
        setDataRemained(data.length);
        if (validateParamaters(data, chmLzxcResetTable)) {
            /* unmarshal fields */
            chmLzxcResetTable.setVersion(unmarshalUInt32(data, chmLzxcResetTable.getVersion()));
            chmLzxcResetTable.setBlockCount(unmarshalUInt32(data, chmLzxcResetTable.getBlockCount()));
            chmLzxcResetTable.setUnknown(unmarshalUInt32(data, chmLzxcResetTable.getUnknown()));
            chmLzxcResetTable.setTableOffset(unmarshalUInt32(data, chmLzxcResetTable.getTableOffset()));
            chmLzxcResetTable.setUncompressedLen(unmarshalUint64(data, chmLzxcResetTable.getUncompressedLen()));
            chmLzxcResetTable.setCompressedLen(unmarshalUint64(data, chmLzxcResetTable.getCompressedLen()));
            chmLzxcResetTable.setBlockLlen(unmarshalUint64(data, chmLzxcResetTable.getBlockLen()));
            chmLzxcResetTable.setBlockAddress(enumerateBlockAddresses(data));
        }

        /* checks chmLzxcResetTable */
        if (chmLzxcResetTable.getVersion() != ChmConstants.CHM_VER_2)
            throw new ChmParsingException(
                    "does not seem currect version of chmLzxcResetTable");
    }
}
