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

package com.sobot.chat.widget.zxing.pdf417.decoder;

import com.sobot.chat.widget.zxing.ChecksumException;
import com.sobot.chat.widget.zxing.FormatException;
import com.sobot.chat.widget.zxing.NotFoundException;
import com.sobot.chat.widget.zxing.ResultPoint;
import com.sobot.chat.widget.zxing.common.BitMatrix;
import com.sobot.chat.widget.zxing.common.DecoderResult;
import com.sobot.chat.widget.zxing.common.detector.MathUtils;
import com.sobot.chat.widget.zxing.pdf417.PDF417Common;
import com.sobot.chat.widget.zxing.pdf417.decoder.BarcodeMetadata;
import com.sobot.chat.widget.zxing.pdf417.decoder.BarcodeValue;
import com.sobot.chat.widget.zxing.pdf417.decoder.BoundingBox;
import com.sobot.chat.widget.zxing.pdf417.decoder.Codeword;
import com.sobot.chat.widget.zxing.pdf417.decoder.DecodedBitStreamParser;
import com.sobot.chat.widget.zxing.pdf417.decoder.DetectionResult;
import com.sobot.chat.widget.zxing.pdf417.decoder.DetectionResultColumn;
import com.sobot.chat.widget.zxing.pdf417.decoder.DetectionResultRowIndicatorColumn;
import com.sobot.chat.widget.zxing.pdf417.decoder.PDF417CodewordDecoder;
import com.sobot.chat.widget.zxing.pdf417.decoder.ec.ErrorCorrection;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Formatter;
import java.util.List;

/**
 * @author Guenther Grau
 */
public final class PDF417ScanningDecoder {

  private static final int CODEWORD_SKEW_SIZE = 2;

  private static final int MAX_ERRORS = 3;
  private static final int MAX_EC_CODEWORDS = 512;
  private static final ErrorCorrection errorCorrection = new ErrorCorrection();

  private PDF417ScanningDecoder() {
  }

  // TODO don't pass in minCodewordWidth and maxCodewordWidth, pass in barcode columns for start and stop pattern
  // columns. That way width can be deducted from the pattern column.
  // This approach also allows to detect more details about the barcode, e.g. if a bar type (white or black) is wider
  // than it should be. This can happen if the scanner used a bad blackpoint.
  public static DecoderResult decode(BitMatrix image,
                                     ResultPoint imageTopLeft,
                                     ResultPoint imageBottomLeft,
                                     ResultPoint imageTopRight,
                                     ResultPoint imageBottomRight,
                                     int minCodewordWidth,
                                     int maxCodewordWidth) throws NotFoundException, FormatException, ChecksumException {
    com.sobot.chat.widget.zxing.pdf417.decoder.BoundingBox boundingBox = new com.sobot.chat.widget.zxing.pdf417.decoder.BoundingBox(image, imageTopLeft, imageBottomLeft, imageTopRight, imageBottomRight);
    DetectionResultRowIndicatorColumn leftRowIndicatorColumn = null;
    DetectionResultRowIndicatorColumn rightRowIndicatorColumn = null;
    com.sobot.chat.widget.zxing.pdf417.decoder.DetectionResult detectionResult;
    for (boolean firstPass = true; ; firstPass = false) {
      if (imageTopLeft != null) {
        leftRowIndicatorColumn = getRowIndicatorColumn(image, boundingBox, imageTopLeft, true, minCodewordWidth,
            maxCodewordWidth);
      }
      if (imageTopRight != null) {
        rightRowIndicatorColumn = getRowIndicatorColumn(image, boundingBox, imageTopRight, false, minCodewordWidth,
            maxCodewordWidth);
      }
      detectionResult = merge(leftRowIndicatorColumn, rightRowIndicatorColumn);
      if (detectionResult == null) {
        throw NotFoundException.getNotFoundInstance();
      }
      com.sobot.chat.widget.zxing.pdf417.decoder.BoundingBox resultBox = detectionResult.getBoundingBox();
      if (firstPass && resultBox != null &&
          (resultBox.getMinY() < boundingBox.getMinY() || resultBox.getMaxY() > boundingBox.getMaxY())) {
        boundingBox = resultBox;
      } else {
        break;
      }
    }
    detectionResult.setBoundingBox(boundingBox);
    int maxBarcodeColumn = detectionResult.getBarcodeColumnCount() + 1;
    detectionResult.setDetectionResultColumn(0, leftRowIndicatorColumn);
    detectionResult.setDetectionResultColumn(maxBarcodeColumn, rightRowIndicatorColumn);

    boolean leftToRight = leftRowIndicatorColumn != null;
    for (int barcodeColumnCount = 1; barcodeColumnCount <= maxBarcodeColumn; barcodeColumnCount++) {
      int barcodeColumn = leftToRight ? barcodeColumnCount : maxBarcodeColumn - barcodeColumnCount;
      if (detectionResult.getDetectionResultColumn(barcodeColumn) != null) {
        // This will be the case for the opposite row indicator column, which doesn't need to be decoded again.
        continue;
      }
      com.sobot.chat.widget.zxing.pdf417.decoder.DetectionResultColumn detectionResultColumn;
      if (barcodeColumn == 0 || barcodeColumn == maxBarcodeColumn) {
        detectionResultColumn = new DetectionResultRowIndicatorColumn(boundingBox, barcodeColumn == 0);
      } else {
        detectionResultColumn = new com.sobot.chat.widget.zxing.pdf417.decoder.DetectionResultColumn(boundingBox);
      }
      detectionResult.setDetectionResultColumn(barcodeColumn, detectionResultColumn);
      int startColumn = -1;
      int previousStartColumn = startColumn;
      // TODO start at a row for which we know the start position, then detect upwards and downwards from there.
      for (int imageRow = boundingBox.getMinY(); imageRow <= boundingBox.getMaxY(); imageRow++) {
        startColumn = getStartColumn(detectionResult, barcodeColumn, imageRow, leftToRight);
        if (startColumn < 0 || startColumn > boundingBox.getMaxX()) {
          if (previousStartColumn == -1) {
            continue;
          }
          startColumn = previousStartColumn;
        }
        com.sobot.chat.widget.zxing.pdf417.decoder.Codeword codeword = detectCodeword(image, boundingBox.getMinX(), boundingBox.getMaxX(), leftToRight,
            startColumn, imageRow, minCodewordWidth, maxCodewordWidth);
        if (codeword != null) {
          detectionResultColumn.setCodeword(imageRow, codeword);
          previousStartColumn = startColumn;
          minCodewordWidth = Math.min(minCodewordWidth, codeword.getWidth());
          maxCodewordWidth = Math.max(maxCodewordWidth, codeword.getWidth());
        }
      }
    }
    return createDecoderResult(detectionResult);
  }

  private static com.sobot.chat.widget.zxing.pdf417.decoder.DetectionResult merge(DetectionResultRowIndicatorColumn leftRowIndicatorColumn,
                                                                       DetectionResultRowIndicatorColumn rightRowIndicatorColumn)
      throws NotFoundException {
    if (leftRowIndicatorColumn == null && rightRowIndicatorColumn == null) {
      return null;
    }
    com.sobot.chat.widget.zxing.pdf417.decoder.BarcodeMetadata barcodeMetadata = getBarcodeMetadata(leftRowIndicatorColumn, rightRowIndicatorColumn);
    if (barcodeMetadata == null) {
      return null;
    }
    com.sobot.chat.widget.zxing.pdf417.decoder.BoundingBox boundingBox = com.sobot.chat.widget.zxing.pdf417.decoder.BoundingBox.merge(adjustBoundingBox(leftRowIndicatorColumn),
        adjustBoundingBox(rightRowIndicatorColumn));
    return new com.sobot.chat.widget.zxing.pdf417.decoder.DetectionResult(barcodeMetadata, boundingBox);
  }

  private static com.sobot.chat.widget.zxing.pdf417.decoder.BoundingBox adjustBoundingBox(DetectionResultRowIndicatorColumn rowIndicatorColumn)
      throws NotFoundException {
    if (rowIndicatorColumn == null) {
      return null;
    }
    int[] rowHeights = rowIndicatorColumn.getRowHeights();
    if (rowHeights == null) {
      return null;
    }
    int maxRowHeight = getMax(rowHeights);
    int missingStartRows = 0;
    for (int rowHeight : rowHeights) {
      missingStartRows += maxRowHeight - rowHeight;
      if (rowHeight > 0) {
        break;
      }
    }
    com.sobot.chat.widget.zxing.pdf417.decoder.Codeword[] codewords = rowIndicatorColumn.getCodewords();
    for (int row = 0; missingStartRows > 0 && codewords[row] == null; row++) {
      missingStartRows--;
    }
    int missingEndRows = 0;
    for (int row = rowHeights.length - 1; row >= 0; row--) {
      missingEndRows += maxRowHeight - rowHeights[row];
      if (rowHeights[row] > 0) {
        break;
      }
    }
    for (int row = codewords.length - 1; missingEndRows > 0 && codewords[row] == null; row--) {
      missingEndRows--;
    }
    return rowIndicatorColumn.getBoundingBox().addMissingRows(missingStartRows, missingEndRows,
        rowIndicatorColumn.isLeft());
  }

  private static int getMax(int[] values) {
    int maxValue = -1;
    for (int value : values) {
      maxValue = Math.max(maxValue, value);
    }
    return maxValue;
  }

  private static com.sobot.chat.widget.zxing.pdf417.decoder.BarcodeMetadata getBarcodeMetadata(DetectionResultRowIndicatorColumn leftRowIndicatorColumn,
                                                                                    DetectionResultRowIndicatorColumn rightRowIndicatorColumn) {
    com.sobot.chat.widget.zxing.pdf417.decoder.BarcodeMetadata leftBarcodeMetadata;
    if (leftRowIndicatorColumn == null ||
        (leftBarcodeMetadata = leftRowIndicatorColumn.getBarcodeMetadata()) == null) {
      return rightRowIndicatorColumn == null ? null : rightRowIndicatorColumn.getBarcodeMetadata();
    }
    com.sobot.chat.widget.zxing.pdf417.decoder.BarcodeMetadata rightBarcodeMetadata;
    if (rightRowIndicatorColumn == null ||
        (rightBarcodeMetadata = rightRowIndicatorColumn.getBarcodeMetadata()) == null) {
      return leftBarcodeMetadata;
    }

    if (leftBarcodeMetadata.getColumnCount() != rightBarcodeMetadata.getColumnCount() &&
        leftBarcodeMetadata.getErrorCorrectionLevel() != rightBarcodeMetadata.getErrorCorrectionLevel() &&
        leftBarcodeMetadata.getRowCount() != rightBarcodeMetadata.getRowCount()) {
      return null;
    }
    return leftBarcodeMetadata;
  }

  private static DetectionResultRowIndicatorColumn getRowIndicatorColumn(BitMatrix image,
                                                                         com.sobot.chat.widget.zxing.pdf417.decoder.BoundingBox boundingBox,
                                                                         ResultPoint startPoint,
                                                                         boolean leftToRight,
                                                                         int minCodewordWidth,
                                                                         int maxCodewordWidth) {
    DetectionResultRowIndicatorColumn rowIndicatorColumn = new DetectionResultRowIndicatorColumn(boundingBox,
        leftToRight);
    for (int i = 0; i < 2; i++) {
      int increment = i == 0 ? 1 : -1;
      int startColumn = (int) startPoint.getX();
      for (int imageRow = (int) startPoint.getY(); imageRow <= boundingBox.getMaxY() &&
          imageRow >= boundingBox.getMinY(); imageRow += increment) {
        com.sobot.chat.widget.zxing.pdf417.decoder.Codeword codeword = detectCodeword(image, 0, image.getWidth(), leftToRight, startColumn, imageRow,
            minCodewordWidth, maxCodewordWidth);
        if (codeword != null) {
          rowIndicatorColumn.setCodeword(imageRow, codeword);
          if (leftToRight) {
            startColumn = codeword.getStartX();
          } else {
            startColumn = codeword.getEndX();
          }
        }
      }
    }
    return rowIndicatorColumn;
  }

  private static void adjustCodewordCount(com.sobot.chat.widget.zxing.pdf417.decoder.DetectionResult detectionResult, com.sobot.chat.widget.zxing.pdf417.decoder.BarcodeValue[][] barcodeMatrix)
      throws NotFoundException {
    com.sobot.chat.widget.zxing.pdf417.decoder.BarcodeValue barcodeMatrix01 = barcodeMatrix[0][1];
    int[] numberOfCodewords = barcodeMatrix01.getValue();
    int calculatedNumberOfCodewords = detectionResult.getBarcodeColumnCount() *
        detectionResult.getBarcodeRowCount() -
        getNumberOfECCodeWords(detectionResult.getBarcodeECLevel());
    if (numberOfCodewords.length == 0) {
      if (calculatedNumberOfCodewords < 1 || calculatedNumberOfCodewords > PDF417Common.MAX_CODEWORDS_IN_BARCODE) {
        throw NotFoundException.getNotFoundInstance();
      }
      barcodeMatrix01.setValue(calculatedNumberOfCodewords);
    } else if (numberOfCodewords[0] != calculatedNumberOfCodewords) {
      // The calculated one is more reliable as it is derived from the row indicator columns
      barcodeMatrix01.setValue(calculatedNumberOfCodewords);
    }
  }

  private static DecoderResult createDecoderResult(com.sobot.chat.widget.zxing.pdf417.decoder.DetectionResult detectionResult) throws FormatException,
      ChecksumException, NotFoundException {
    com.sobot.chat.widget.zxing.pdf417.decoder.BarcodeValue[][] barcodeMatrix = createBarcodeMatrix(detectionResult);
    adjustCodewordCount(detectionResult, barcodeMatrix);
    Collection<Integer> erasures = new ArrayList<>();
    int[] codewords = new int[detectionResult.getBarcodeRowCount() * detectionResult.getBarcodeColumnCount()];
    List<int[]> ambiguousIndexValuesList = new ArrayList<>();
    Collection<Integer> ambiguousIndexesList = new ArrayList<>();
    for (int row = 0; row < detectionResult.getBarcodeRowCount(); row++) {
      for (int column = 0; column < detectionResult.getBarcodeColumnCount(); column++) {
        int[] values = barcodeMatrix[row][column + 1].getValue();
        int codewordIndex = row * detectionResult.getBarcodeColumnCount() + column;
        if (values.length == 0) {
          erasures.add(codewordIndex);
        } else if (values.length == 1) {
          codewords[codewordIndex] = values[0];
        } else {
          ambiguousIndexesList.add(codewordIndex);
          ambiguousIndexValuesList.add(values);
        }
      }
    }
    int[][] ambiguousIndexValues = new int[ambiguousIndexValuesList.size()][];
    for (int i = 0; i < ambiguousIndexValues.length; i++) {
      ambiguousIndexValues[i] = ambiguousIndexValuesList.get(i);
    }
    return createDecoderResultFromAmbiguousValues(detectionResult.getBarcodeECLevel(), codewords,
        PDF417Common.toIntArray(erasures), PDF417Common.toIntArray(ambiguousIndexesList), ambiguousIndexValues);
  }

  /**
   * This method deals with the fact, that the decoding process doesn't always yield a single most likely value. The
   * current error correction implementation doesn't deal with erasures very well, so it's better to provide a value
   * for these ambiguous codewords instead of treating it as an erasure. The problem is that we don't know which of
   * the ambiguous values to choose. We try decode using the first value, and if that fails, we use another of the
   * ambiguous values and try to decode again. This usually only happens on very hard to read and decode barcodes,
   * so decoding the normal barcodes is not affected by this.
   *
   * @param erasureArray contains the indexes of erasures
   * @param ambiguousIndexes array with the indexes that have more than one most likely value
   * @param ambiguousIndexValues two dimensional array that contains the ambiguous values. The first dimension must
   * be the same length as the ambiguousIndexes array
   */
  private static DecoderResult createDecoderResultFromAmbiguousValues(int ecLevel,
                                                                      int[] codewords,
                                                                      int[] erasureArray,
                                                                      int[] ambiguousIndexes,
                                                                      int[][] ambiguousIndexValues)
      throws FormatException, ChecksumException {
    int[] ambiguousIndexCount = new int[ambiguousIndexes.length];

    int tries = 100;
    while (tries-- > 0) {
      for (int i = 0; i < ambiguousIndexCount.length; i++) {
        codewords[ambiguousIndexes[i]] = ambiguousIndexValues[i][ambiguousIndexCount[i]];
      }
      try {
        return decodeCodewords(codewords, ecLevel, erasureArray);
      } catch (ChecksumException ignored) {
        //
      }
      if (ambiguousIndexCount.length == 0) {
        throw ChecksumException.getChecksumInstance();
      }
      for (int i = 0; i < ambiguousIndexCount.length; i++) {
        if (ambiguousIndexCount[i] < ambiguousIndexValues[i].length - 1) {
          ambiguousIndexCount[i]++;
          break;
        } else {
          ambiguousIndexCount[i] = 0;
          if (i == ambiguousIndexCount.length - 1) {
            throw ChecksumException.getChecksumInstance();
          }
        }
      }
    }
    throw ChecksumException.getChecksumInstance();
  }

  private static com.sobot.chat.widget.zxing.pdf417.decoder.BarcodeValue[][] createBarcodeMatrix(com.sobot.chat.widget.zxing.pdf417.decoder.DetectionResult detectionResult) {
    com.sobot.chat.widget.zxing.pdf417.decoder.BarcodeValue[][] barcodeMatrix =
        new com.sobot.chat.widget.zxing.pdf417.decoder.BarcodeValue[detectionResult.getBarcodeRowCount()][detectionResult.getBarcodeColumnCount() + 2];
    for (int row = 0; row < barcodeMatrix.length; row++) {
      for (int column = 0; column < barcodeMatrix[row].length; column++) {
        barcodeMatrix[row][column] = new com.sobot.chat.widget.zxing.pdf417.decoder.BarcodeValue();
      }
    }

    int column = 0;
    for (com.sobot.chat.widget.zxing.pdf417.decoder.DetectionResultColumn detectionResultColumn : detectionResult.getDetectionResultColumns()) {
      if (detectionResultColumn != null) {
        for (com.sobot.chat.widget.zxing.pdf417.decoder.Codeword codeword : detectionResultColumn.getCodewords()) {
          if (codeword != null) {
            int rowNumber = codeword.getRowNumber();
            if (rowNumber >= 0) {
              if (rowNumber >= barcodeMatrix.length) {
                // We have more rows than the barcode metadata allows for, ignore them.
                continue;
              }
              barcodeMatrix[rowNumber][column].setValue(codeword.getValue());
            }
          }
        }
      }
      column++;
    }
    return barcodeMatrix;
  }

  private static boolean isValidBarcodeColumn(com.sobot.chat.widget.zxing.pdf417.decoder.DetectionResult detectionResult, int barcodeColumn) {
    return barcodeColumn >= 0 && barcodeColumn <= detectionResult.getBarcodeColumnCount() + 1;
  }

  private static int getStartColumn(com.sobot.chat.widget.zxing.pdf417.decoder.DetectionResult detectionResult,
                                    int barcodeColumn,
                                    int imageRow,
                                    boolean leftToRight) {
    int offset = leftToRight ? 1 : -1;
    com.sobot.chat.widget.zxing.pdf417.decoder.Codeword codeword = null;
    if (isValidBarcodeColumn(detectionResult, barcodeColumn - offset)) {
      codeword = detectionResult.getDetectionResultColumn(barcodeColumn - offset).getCodeword(imageRow);
    }
    if (codeword != null) {
      return leftToRight ? codeword.getEndX() : codeword.getStartX();
    }
    codeword = detectionResult.getDetectionResultColumn(barcodeColumn).getCodewordNearby(imageRow);
    if (codeword != null) {
      return leftToRight ? codeword.getStartX() : codeword.getEndX();
    }
    if (isValidBarcodeColumn(detectionResult, barcodeColumn - offset)) {
      codeword = detectionResult.getDetectionResultColumn(barcodeColumn - offset).getCodewordNearby(imageRow);
    }
    if (codeword != null) {
      return leftToRight ? codeword.getEndX() : codeword.getStartX();
    }
    int skippedColumns = 0;

    while (isValidBarcodeColumn(detectionResult, barcodeColumn - offset)) {
      barcodeColumn -= offset;
      for (com.sobot.chat.widget.zxing.pdf417.decoder.Codeword previousRowCodeword : detectionResult.getDetectionResultColumn(barcodeColumn).getCodewords()) {
        if (previousRowCodeword != null) {
          return (leftToRight ? previousRowCodeword.getEndX() : previousRowCodeword.getStartX()) +
              offset *
              skippedColumns *
              (previousRowCodeword.getEndX() - previousRowCodeword.getStartX());
        }
      }
      skippedColumns++;
    }
    return leftToRight ? detectionResult.getBoundingBox().getMinX() : detectionResult.getBoundingBox().getMaxX();
  }

  private static com.sobot.chat.widget.zxing.pdf417.decoder.Codeword detectCodeword(BitMatrix image,
                                                                         int minColumn,
                                                                         int maxColumn,
                                                                         boolean leftToRight,
                                                                         int startColumn,
                                                                         int imageRow,
                                                                         int minCodewordWidth,
                                                                         int maxCodewordWidth) {
    startColumn = adjustCodewordStartColumn(image, minColumn, maxColumn, leftToRight, startColumn, imageRow);
    // we usually know fairly exact now how long a codeword is. We should provide minimum and maximum expected length
    // and try to adjust the read pixels, e.g. remove single pixel errors or try to cut off exceeding pixels.
    // min and maxCodewordWidth should not be used as they are calculated for the whole barcode an can be inaccurate
    // for the current position
    int[] moduleBitCount = getModuleBitCount(image, minColumn, maxColumn, leftToRight, startColumn, imageRow);
    if (moduleBitCount == null) {
      return null;
    }
    int endColumn;
    int codewordBitCount = MathUtils.sum(moduleBitCount);
    if (leftToRight) {
      endColumn = startColumn + codewordBitCount;
    } else {
      for (int i = 0; i < moduleBitCount.length / 2; i++) {
        int tmpCount = moduleBitCount[i];
        moduleBitCount[i] = moduleBitCount[moduleBitCount.length - 1 - i];
        moduleBitCount[moduleBitCount.length - 1 - i] = tmpCount;
      }
      endColumn = startColumn;
      startColumn = endColumn - codewordBitCount;
    }
    // TODO implement check for width and correction of black and white bars
    // use start (and maybe stop pattern) to determine if black bars are wider than white bars. If so, adjust.
    // should probably done only for codewords with a lot more than 17 bits.
    // The following fixes 10-1.png, which has wide black bars and small white bars
    //    for (int i = 0; i < moduleBitCount.length; i++) {
    //      if (i % 2 == 0) {
    //        moduleBitCount[i]--;
    //      } else {
    //        moduleBitCount[i]++;
    //      }
    //    }

    // We could also use the width of surrounding codewords for more accurate results, but this seems
    // sufficient for now
    if (!checkCodewordSkew(codewordBitCount, minCodewordWidth, maxCodewordWidth)) {
      // We could try to use the startX and endX position of the codeword in the same column in the previous row,
      // create the bit count from it and normalize it to 8. This would help with single pixel errors.
      return null;
    }

    int decodedValue = com.sobot.chat.widget.zxing.pdf417.decoder.PDF417CodewordDecoder.getDecodedValue(moduleBitCount);
    int codeword = PDF417Common.getCodeword(decodedValue);
    if (codeword == -1) {
      return null;
    }
    return new com.sobot.chat.widget.zxing.pdf417.decoder.Codeword(startColumn, endColumn, getCodewordBucketNumber(decodedValue), codeword);
  }

  private static int[] getModuleBitCount(BitMatrix image,
                                         int minColumn,
                                         int maxColumn,
                                         boolean leftToRight,
                                         int startColumn,
                                         int imageRow) {
    int imageColumn = startColumn;
    int[] moduleBitCount = new int[8];
    int moduleNumber = 0;
    int increment = leftToRight ? 1 : -1;
    boolean previousPixelValue = leftToRight;
    while ((leftToRight ? imageColumn < maxColumn : imageColumn >= minColumn) &&
           moduleNumber < moduleBitCount.length) {
      if (image.get(imageColumn, imageRow) == previousPixelValue) {
        moduleBitCount[moduleNumber]++;
        imageColumn += increment;
      } else {
        moduleNumber++;
        previousPixelValue = !previousPixelValue;
      }
    }
    if (moduleNumber == moduleBitCount.length ||
        ((imageColumn == (leftToRight ? maxColumn : minColumn)) &&
         moduleNumber == moduleBitCount.length - 1)) {
      return moduleBitCount;
    }
    return null;
  }

  private static int getNumberOfECCodeWords(int barcodeECLevel) {
    return 2 << barcodeECLevel;
  }

  private static int adjustCodewordStartColumn(BitMatrix image,
                                               int minColumn,
                                               int maxColumn,
                                               boolean leftToRight,
                                               int codewordStartColumn,
                                               int imageRow) {
    int correctedStartColumn = codewordStartColumn;
    int increment = leftToRight ? -1 : 1;
    // there should be no black pixels before the start column. If there are, then we need to start earlier.
    for (int i = 0; i < 2; i++) {
      while ((leftToRight ? correctedStartColumn >= minColumn : correctedStartColumn < maxColumn) &&
             leftToRight == image.get(correctedStartColumn, imageRow)) {
        if (Math.abs(codewordStartColumn - correctedStartColumn) > CODEWORD_SKEW_SIZE) {
          return codewordStartColumn;
        }
        correctedStartColumn += increment;
      }
      increment = -increment;
      leftToRight = !leftToRight;
    }
    return correctedStartColumn;
  }

  private static boolean checkCodewordSkew(int codewordSize, int minCodewordWidth, int maxCodewordWidth) {
    return minCodewordWidth - CODEWORD_SKEW_SIZE <= codewordSize &&
        codewordSize <= maxCodewordWidth + CODEWORD_SKEW_SIZE;
  }

  private static DecoderResult decodeCodewords(int[] codewords, int ecLevel, int[] erasures) throws FormatException,
      ChecksumException {
    if (codewords.length == 0) {
      throw FormatException.getFormatInstance();
    }

    int numECCodewords = 1 << (ecLevel + 1);
    int correctedErrorsCount = correctErrors(codewords, erasures, numECCodewords);
    verifyCodewordCount(codewords, numECCodewords);

    // Decode the codewords
    DecoderResult decoderResult = DecodedBitStreamParser.decode(codewords, String.valueOf(ecLevel));
    decoderResult.setErrorsCorrected(correctedErrorsCount);
    decoderResult.setErasures(erasures.length);
    return decoderResult;
  }

  /**
   * <p>Given data and error-correction codewords received, possibly corrupted by errors, attempts to
   * correct the errors in-place.</p>
   *
   * @param codewords   data and error correction codewords
   * @param erasures positions of any known erasures
   * @param numECCodewords number of error correction codewords that are available in codewords
   * @throws ChecksumException if error correction fails
   */
  private static int correctErrors(int[] codewords, int[] erasures, int numECCodewords) throws ChecksumException {
    if (erasures != null &&
        erasures.length > numECCodewords / 2 + MAX_ERRORS ||
        numECCodewords < 0 ||
        numECCodewords > MAX_EC_CODEWORDS) {
      // Too many errors or EC Codewords is corrupted
      throw ChecksumException.getChecksumInstance();
    }
    return errorCorrection.decode(codewords, numECCodewords, erasures);
  }

  /**
   * Verify that all is OK with the codeword array.
   */
  private static void verifyCodewordCount(int[] codewords, int numECCodewords) throws FormatException {
    if (codewords.length < 4) {
      // Codeword array size should be at least 4 allowing for
      // Count CW, At least one Data CW, Error Correction CW, Error Correction CW
      throw FormatException.getFormatInstance();
    }
    // The first codeword, the Symbol Length Descriptor, shall always encode the total number of data
    // codewords in the symbol, including the Symbol Length Descriptor itself, data codewords and pad
    // codewords, but excluding the number of error correction codewords.
    int numberOfCodewords = codewords[0];
    if (numberOfCodewords > codewords.length) {
      throw FormatException.getFormatInstance();
    }
    if (numberOfCodewords == 0) {
      // Reset to the length of the array - 8 (Allow for at least level 3 Error Correction (8 Error Codewords)
      if (numECCodewords < codewords.length) {
        codewords[0] = codewords.length - numECCodewords;
      } else {
        throw FormatException.getFormatInstance();
      }
    }
  }

  private static int[] getBitCountForCodeword(int codeword) {
    int[] result = new int[8];
    int previousValue = 0;
    int i = result.length - 1;
    while (true) {
      if ((codeword & 0x1) != previousValue) {
        previousValue = codeword & 0x1;
        i--;
        if (i < 0) {
          break;
        }
      }
      result[i]++;
      codeword >>= 1;
    }
    return result;
  }

  private static int getCodewordBucketNumber(int codeword) {
    return getCodewordBucketNumber(getBitCountForCodeword(codeword));
  }

  private static int getCodewordBucketNumber(int[] moduleBitCount) {
    return (moduleBitCount[0] - moduleBitCount[2] + moduleBitCount[4] - moduleBitCount[6] + 9) % 9;
  }

  public static String toString(com.sobot.chat.widget.zxing.pdf417.decoder.BarcodeValue[][] barcodeMatrix) {
    try (Formatter formatter = new Formatter()) {
      for (int row = 0; row < barcodeMatrix.length; row++) {
        formatter.format("Row %2d: ", row);
        for (int column = 0; column < barcodeMatrix[row].length; column++) {
          com.sobot.chat.widget.zxing.pdf417.decoder.BarcodeValue barcodeValue = barcodeMatrix[row][column];
          if (barcodeValue.getValue().length == 0) {
            formatter.format("        ", (Object[]) null);
          } else {
            formatter.format("%4d(%2d)", barcodeValue.getValue()[0],
                barcodeValue.getConfidence(barcodeValue.getValue()[0]));
          }
        }
        formatter.format("%n");
      }
      return formatter.toString();
    }
  }

}
