/*
 * Copyright 2010 ZXing authors
 *
 * Licensed 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.
 */
import DecoderResult from '../../common/DecoderResult';
import GenericGF from '../../common/reedsolomon/GenericGF';
import ReedSolomonDecoder from '../../common/reedsolomon/ReedSolomonDecoder';
import IllegalStateException from '../../IllegalStateException';
import FormatException from '../../FormatException';
import StringUtils from '../../common/StringUtils';
import Integer from '../../util/Integer';
// import java.util.Arrays;
var Table;
(function (Table) {
    Table[Table["UPPER"] = 0] = "UPPER";
    Table[Table["LOWER"] = 1] = "LOWER";
    Table[Table["MIXED"] = 2] = "MIXED";
    Table[Table["DIGIT"] = 3] = "DIGIT";
    Table[Table["PUNCT"] = 4] = "PUNCT";
    Table[Table["BINARY"] = 5] = "BINARY";
})(Table || (Table = {}));
/**
 * <p>The main class which implements Aztec Code decoding -- as opposed to locating and extracting
 * the Aztec Code from an image.</p>
 *
 * @author David Olivier
 */
export default class Decoder {
    decode(detectorResult) {
        this.ddata = detectorResult;
        let matrix = detectorResult.getBits();
        let rawbits = this.extractBits(matrix);
        let correctedBits = this.correctBits(rawbits);
        let rawBytes = Decoder.convertBoolArrayToByteArray(correctedBits);
        let result = Decoder.getEncodedData(correctedBits);
        let decoderResult = new DecoderResult(rawBytes, result, null, null);
        decoderResult.setNumBits(correctedBits.length);
        return decoderResult;
    }
    // This method is used for testing the high-level encoder
    static highLevelDecode(correctedBits) {
        return this.getEncodedData(correctedBits);
    }
    /**
     * Gets the string encoded in the aztec code bits
     *
     * @return the decoded string
     */
    static getEncodedData(correctedBits) {
        let endIndex = correctedBits.length;
        let latchTable = Table.UPPER; // table most recently latched to
        let shiftTable = Table.UPPER; // table to use for the next read
        let result = '';
        let index = 0;
        while (index < endIndex) {
            if (shiftTable === Table.BINARY) {
                if (endIndex - index < 5) {
                    break;
                }
                let length = Decoder.readCode(correctedBits, index, 5);
                index += 5;
                if (length === 0) {
                    if (endIndex - index < 11) {
                        break;
                    }
                    length = Decoder.readCode(correctedBits, index, 11) + 31;
                    index += 11;
                }
                for (let charCount = 0; charCount < length; charCount++) {
                    if (endIndex - index < 8) {
                        index = endIndex; // Force outer loop to exit
                        break;
                    }
                    const code = Decoder.readCode(correctedBits, index, 8);
                    result += /*(char)*/ StringUtils.castAsNonUtf8Char(code);
                    index += 8;
                }
                // Go back to whatever mode we had been in
                shiftTable = latchTable;
            }
            else {
                let size = shiftTable === Table.DIGIT ? 4 : 5;
                if (endIndex - index < size) {
                    break;
                }
                let code = Decoder.readCode(correctedBits, index, size);
                index += size;
                let str = Decoder.getCharacter(shiftTable, code);
                if (str.startsWith('CTRL_')) {
                    // Table changes
                    // ISO/IEC 24778:2008 prescribes ending a shift sequence in the mode from which it was invoked.
                    // That's including when that mode is a shift.
                    // Our test case dlusbs.png for issue #642 exercises that.
                    latchTable = shiftTable; // Latch the current mode, so as to return to Upper after U/S B/S
                    shiftTable = Decoder.getTable(str.charAt(5));
                    if (str.charAt(6) === 'L') {
                        latchTable = shiftTable;
                    }
                }
                else {
                    result += str;
                    // Go back to whatever mode we had been in
                    shiftTable = latchTable;
                }
            }
        }
        return result;
    }
    /**
     * gets the table corresponding to the char passed
     */
    static getTable(t) {
        switch (t) {
            case 'L':
                return Table.LOWER;
            case 'P':
                return Table.PUNCT;
            case 'M':
                return Table.MIXED;
            case 'D':
                return Table.DIGIT;
            case 'B':
                return Table.BINARY;
            case 'U':
            default:
                return Table.UPPER;
        }
    }
    /**
     * Gets the character (or string) corresponding to the passed code in the given table
     *
     * @param table the table used
     * @param code the code of the character
     */
    static getCharacter(table, code) {
        switch (table) {
            case Table.UPPER:
                return Decoder.UPPER_TABLE[code];
            case Table.LOWER:
                return Decoder.LOWER_TABLE[code];
            case Table.MIXED:
                return Decoder.MIXED_TABLE[code];
            case Table.PUNCT:
                return Decoder.PUNCT_TABLE[code];
            case Table.DIGIT:
                return Decoder.DIGIT_TABLE[code];
            default:
                // Should not reach here.
                throw new IllegalStateException('Bad table');
        }
    }
    /**
     * <p>Performs RS error correction on an array of bits.</p>
     *
     * @return the corrected array
     * @throws FormatException if the input contains too many errors
     */
    correctBits(rawbits) {
        let gf;
        let codewordSize;
        if (this.ddata.getNbLayers() <= 2) {
            codewordSize = 6;
            gf = GenericGF.AZTEC_DATA_6;
        }
        else if (this.ddata.getNbLayers() <= 8) {
            codewordSize = 8;
            gf = GenericGF.AZTEC_DATA_8;
        }
        else if (this.ddata.getNbLayers() <= 22) {
            codewordSize = 10;
            gf = GenericGF.AZTEC_DATA_10;
        }
        else {
            codewordSize = 12;
            gf = GenericGF.AZTEC_DATA_12;
        }
        let numDataCodewords = this.ddata.getNbDatablocks();
        let numCodewords = rawbits.length / codewordSize;
        if (numCodewords < numDataCodewords) {
            throw new FormatException();
        }
        let offset = rawbits.length % codewordSize;
        let dataWords = new Int32Array(numCodewords);
        for (let i = 0; i < numCodewords; i++, offset += codewordSize) {
            dataWords[i] = Decoder.readCode(rawbits, offset, codewordSize);
        }
        try {
            let rsDecoder = new ReedSolomonDecoder(gf);
            rsDecoder.decode(dataWords, numCodewords - numDataCodewords);
        }
        catch (ex) {
            throw new FormatException(ex);
        }
        // Now perform the unstuffing operation.
        // First, count how many bits are going to be thrown out as stuffing
        let mask = (1 << codewordSize) - 1;
        let stuffedBits = 0;
        for (let i = 0; i < numDataCodewords; i++) {
            let dataWord = dataWords[i];
            if (dataWord === 0 || dataWord === mask) {
                throw new FormatException();
            }
            else if (dataWord === 1 || dataWord === mask - 1) {
                stuffedBits++;
            }
        }
        // Now, actually unpack the bits and remove the stuffing
        let correctedBits = new Array(numDataCodewords * codewordSize - stuffedBits);
        let index = 0;
        for (let i = 0; i < numDataCodewords; i++) {
            let dataWord = dataWords[i];
            if (dataWord === 1 || dataWord === mask - 1) {
                // next codewordSize-1 bits are all zeros or all ones
                correctedBits.fill(dataWord > 1, index, index + codewordSize - 1);
                // Arrays.fill(correctedBits, index, index + codewordSize - 1, dataWord > 1);
                index += codewordSize - 1;
            }
            else {
                for (let bit = codewordSize - 1; bit >= 0; --bit) {
                    correctedBits[index++] = (dataWord & (1 << bit)) !== 0;
                }
            }
        }
        return correctedBits;
    }
    /**
     * Gets the array of bits from an Aztec Code matrix
     *
     * @return the array of bits
     */
    extractBits(matrix) {
        let compact = this.ddata.isCompact();
        let layers = this.ddata.getNbLayers();
        let baseMatrixSize = (compact ? 11 : 14) + layers * 4; // not including alignment lines
        let alignmentMap = new Int32Array(baseMatrixSize);
        let rawbits = new Array(this.totalBitsInLayer(layers, compact));
        if (compact) {
            for (let i = 0; i < alignmentMap.length; i++) {
                alignmentMap[i] = i;
            }
        }
        else {
            let matrixSize = baseMatrixSize + 1 + 2 * Integer.truncDivision((Integer.truncDivision(baseMatrixSize, 2) - 1), 15);
            let origCenter = baseMatrixSize / 2;
            let center = Integer.truncDivision(matrixSize, 2);
            for (let i = 0; i < origCenter; i++) {
                let newOffset = i + Integer.truncDivision(i, 15);
                alignmentMap[origCenter - i - 1] = center - newOffset - 1;
                alignmentMap[origCenter + i] = center + newOffset + 1;
            }
        }
        for (let i = 0, rowOffset = 0; i < layers; i++) {
            let rowSize = (layers - i) * 4 + (compact ? 9 : 12);
            // The top-left most point of this layer is <low, low> (not including alignment lines)
            let low = i * 2;
            // The bottom-right most point of this layer is <high, high> (not including alignment lines)
            let high = baseMatrixSize - 1 - low;
            // We pull bits from the two 2 x rowSize columns and two rowSize x 2 rows
            for (let j = 0; j < rowSize; j++) {
                let columnOffset = j * 2;
                for (let k = 0; k < 2; k++) {
                    // left column
                    rawbits[rowOffset + columnOffset + k] =
                        matrix.get(alignmentMap[low + k], alignmentMap[low + j]);
                    // bottom row
                    rawbits[rowOffset + 2 * rowSize + columnOffset + k] =
                        matrix.get(alignmentMap[low + j], alignmentMap[high - k]);
                    // right column
                    rawbits[rowOffset + 4 * rowSize + columnOffset + k] =
                        matrix.get(alignmentMap[high - k], alignmentMap[high - j]);
                    // top row
                    rawbits[rowOffset + 6 * rowSize + columnOffset + k] =
                        matrix.get(alignmentMap[high - j], alignmentMap[low + k]);
                }
            }
            rowOffset += rowSize * 8;
        }
        return rawbits;
    }
    /**
     * Reads a code of given length and at given index in an array of bits
     */
    static readCode(rawbits, startIndex, length) {
        let res = 0;
        for (let i = startIndex; i < startIndex + length; i++) {
            res <<= 1;
            if (rawbits[i]) {
                res |= 0x01;
            }
        }
        return res;
    }
    /**
     * Reads a code of length 8 in an array of bits, padding with zeros
     */
    static readByte(rawbits, startIndex) {
        let n = rawbits.length - startIndex;
        if (n >= 8) {
            return Decoder.readCode(rawbits, startIndex, 8);
        }
        return Decoder.readCode(rawbits, startIndex, n) << (8 - n);
    }
    /**
     * Packs a bit array into bytes, most significant bit first
     */
    static convertBoolArrayToByteArray(boolArr) {
        let byteArr = new Uint8Array((boolArr.length + 7) / 8);
        for (let i = 0; i < byteArr.length; i++) {
            byteArr[i] = Decoder.readByte(boolArr, 8 * i);
        }
        return byteArr;
    }
    totalBitsInLayer(layers, compact) {
        return ((compact ? 88 : 112) + 16 * layers) * layers;
    }
}
Decoder.UPPER_TABLE = [
    'CTRL_PS', ' ', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
    'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'CTRL_LL', 'CTRL_ML', 'CTRL_DL', 'CTRL_BS'
];
Decoder.LOWER_TABLE = [
    'CTRL_PS', ' ', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p',
    'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'CTRL_US', 'CTRL_ML', 'CTRL_DL', 'CTRL_BS'
];
Decoder.MIXED_TABLE = [
    'CTRL_PS', ' ', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07', '\b', '\t', '\n',
    '\x0b', '\f', '\r', '\x1b', '\x1c', '\x1d', '\x1e', '\x1f', '@', '\\', '^', '_',
    '`', '|', '~', '\x7f', 'CTRL_LL', 'CTRL_UL', 'CTRL_PL', 'CTRL_BS'
];
Decoder.PUNCT_TABLE = [
    '', '\r', '\r\n', '. ', ', ', ': ', '!', '"', '#', '$', '%', '&', '\'', '(', ')',
    '*', '+', ',', '-', '.', '/', ':', ';', '<', '=', '>', '?', '[', ']', '{', '}', 'CTRL_UL'
];
Decoder.DIGIT_TABLE = [
    'CTRL_PS', ' ', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ',', '.', 'CTRL_UL', 'CTRL_US'
];
