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

package com.google.zxing.aztec.decoder;

import com.google.zxing.FormatException;
import com.google.zxing.aztec.AztecDetectorResult;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.common.DecoderResult;
import com.google.zxing.common.reedsolomon.GenericGF;
import com.google.zxing.common.reedsolomon.ReedSolomonDecoder;
import com.google.zxing.common.reedsolomon.ReedSolomonException;

/**
 * <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
 */
public final class Decoder {

  private enum Table {
    UPPER,
    LOWER,
    MIXED,
    DIGIT,
    PUNCT,
    BINARY
  }

  private static final int[] NB_BITS_COMPACT = {
      0, 104, 240, 408, 608
  };

  private static final int[] NB_BITS = {
      0, 128, 288, 480, 704, 960, 1248, 1568, 1920, 2304, 2720, 3168, 3648, 4160, 4704, 5280, 5888, 6528,
      7200, 7904, 8640, 9408, 10208, 11040, 11904, 12800, 13728, 14688, 15680, 16704, 17760, 18848, 19968
  };

  private static final int[] NB_DATABLOCK_COMPACT = {
      0, 17, 40, 51, 76
  };

  private static final int[] NB_DATABLOCK = {
      0, 21, 48, 60, 88, 120, 156, 196, 240, 230, 272, 316, 364, 416, 470, 528, 588, 652, 720, 790, 864,
      940, 1020, 920, 992, 1066, 1144, 1224, 1306, 1392, 1480, 1570, 1664
  };

  private static final String[] 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"
  };

  private static final String[] 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"
  };

  private static final String[] MIXED_TABLE = {
      "CTRL_PS", " ", "\1", "\2", "\3", "\4", "\5", "\6", "\7", "\b", "\t", "\n",
      "\13", "\f", "\r", "\33", "\34", "\35", "\36", "\37", "@", "\\", "^", "_",
      "`", "|", "~", "\177", "CTRL_LL", "CTRL_UL", "CTRL_PL", "CTRL_BS"
  };

  private static final String[] PUNCT_TABLE = {
      "", "\r", "\r\n", ". ", ", ", ": ", "!", "\"", "#", "$", "%", "&", "'", "(", ")",
      "*", "+", ",", "-", ".", "/", ":", ";", "<", "=", ">", "?", "[", "]", "{", "}", "CTRL_UL"
  };

  private static final String[] DIGIT_TABLE = {
      "CTRL_PS", " ", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", ",", ".", "CTRL_UL", "CTRL_US"
  };

  private int numCodewords;
  private int codewordSize;
  private AztecDetectorResult ddata;
  private int invertedBitCount;

  public DecoderResult decode(AztecDetectorResult detectorResult) throws FormatException {
    ddata = detectorResult;
    BitMatrix matrix = detectorResult.getBits();

    if (!ddata.isCompact()) {
      matrix = removeDashedLines(ddata.getBits());
    }

    boolean[] rawbits = extractBits(matrix);

    boolean[] correctedBits = correctBits(rawbits);

    String result = getEncodedData(correctedBits);

    return new DecoderResult(null, result, null, null);
  }

  /**
   * Gets the string encoded in the aztec code bits
   *
   * @return the decoded string
   * @throws FormatException if the input is not valid
   */
  private String getEncodedData(boolean[] correctedBits) throws FormatException {

    int endIndex = codewordSize * ddata.getNbDatablocks() - invertedBitCount;
    if (endIndex > correctedBits.length) {
      throw FormatException.getFormatInstance();
    }

    Table lastTable = Table.UPPER;
    Table table = Table.UPPER;
    int startIndex = 0;
    StringBuilder result = new StringBuilder(20);
    boolean end = false;
    boolean shift = false;
    boolean switchShift = false;
    boolean binaryShift = false;

    while (!end) {

      if (shift) {
        // the table is for the next character only
        switchShift = true;
      } else {
        // save the current table in case next one is a shift
        lastTable = table;
      }

      int code;
      if (binaryShift) {
        if (endIndex - startIndex < 5) {
          break;
        }

        int length = readCode(correctedBits, startIndex, 5);
        startIndex += 5;
        if (length == 0) {
          if (endIndex - startIndex < 11) {
            break;
          }

          length = readCode(correctedBits, startIndex, 11) + 31;
          startIndex += 11;
        }
        for (int charCount = 0; charCount < length; charCount++) {
          if (endIndex - startIndex < 8) {
            end = true;
            break;
          }

          code = readCode(correctedBits, startIndex, 8);
          result.append((char) code);
          startIndex += 8;
        }
        binaryShift = false;
      } else {
        if (table == Table.BINARY) {
          if (endIndex - startIndex < 8) {
            break;
          }
          code = readCode(correctedBits, startIndex, 8);
          startIndex += 8;

          result.append((char) code);

        } else {
          int size = 5;

          if (table == Table.DIGIT) {
            size = 4;
          }

          if (endIndex - startIndex < size) {
            break;
          }

          code = readCode(correctedBits, startIndex, size);
          startIndex += size;

          String str = getCharacter(table, code);
          if (str.startsWith("CTRL_")) {
            // Table changes
            table = getTable(str.charAt(5));

            if (str.charAt(6) == 'S') {
              shift = true;
              if (str.charAt(5) == 'B') {
                binaryShift = true;
              }
            }
          } else {
            result.append(str);
          }


        }
      }

      if (switchShift) {
        table = lastTable;
        shift = false;
        switchShift = false;
      }

    }
    return result.toString();
  }


  /**
   * gets the table corresponding to the char passed
   */
  private static Table getTable(char 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
   */
  private static String getCharacter(Table table, int code) {
    switch (table) {
      case UPPER:
        return UPPER_TABLE[code];
      case LOWER:
        return LOWER_TABLE[code];
      case MIXED:
        return MIXED_TABLE[code];
      case PUNCT:
        return PUNCT_TABLE[code];
      case DIGIT:
        return DIGIT_TABLE[code];
      default:
        return "";
    }
  }

  /**
   * <p>Performs RS error correction on an array of bits.</p>
   *
   * @return the corrected array
   * @throws FormatException if the input contains too many errors
   */
  private boolean[] correctBits(boolean[] rawbits) throws FormatException {
    GenericGF gf;

    if (ddata.getNbLayers() <= 2) {
      codewordSize = 6;
      gf = GenericGF.AZTEC_DATA_6;
    } else if (ddata.getNbLayers() <= 8) {
      codewordSize = 8;
      gf = GenericGF.AZTEC_DATA_8;
    } else if (ddata.getNbLayers() <= 22) {
      codewordSize = 10;
      gf = GenericGF.AZTEC_DATA_10;
    } else {
      codewordSize = 12;
      gf = GenericGF.AZTEC_DATA_12;
    }

    int numDataCodewords = ddata.getNbDatablocks();
    int numECCodewords;
    int offset;

    if (ddata.isCompact()) {
      offset = NB_BITS_COMPACT[ddata.getNbLayers()] - numCodewords * codewordSize;
      numECCodewords = NB_DATABLOCK_COMPACT[ddata.getNbLayers()] - numDataCodewords;
    } else {
      offset = NB_BITS[ddata.getNbLayers()] - numCodewords * codewordSize;
      numECCodewords = NB_DATABLOCK[ddata.getNbLayers()] - numDataCodewords;
    }

    int[] dataWords = new int[numCodewords];
    for (int i = 0; i < numCodewords; i++) {
      int flag = 1;
      for (int j = 1; j <= codewordSize; j++) {
        if (rawbits[codewordSize * i + codewordSize - j + offset]) {
          dataWords[i] += flag;
        }
        flag <<= 1;
      }

      //if (dataWords[i] >= flag) {
      //  flag++;
      //}
    }

    try {
      ReedSolomonDecoder rsDecoder = new ReedSolomonDecoder(gf);
      rsDecoder.decode(dataWords, numECCodewords);
    } catch (ReedSolomonException rse) {
      throw FormatException.getFormatInstance();
    }

    offset = 0;
    invertedBitCount = 0;

    boolean[] correctedBits = new boolean[numDataCodewords * codewordSize];
    for (int i = 0; i < numDataCodewords; i++) {

      boolean seriesColor = false;
      int seriesCount = 0;
      int flag = 1 << (codewordSize - 1);

      for (int j = 0; j < codewordSize; j++) {

        boolean color = (dataWords[i] & flag) == flag;

        if (seriesCount == codewordSize - 1) {

          if (color == seriesColor) {
            //bit must be inverted
            throw FormatException.getFormatInstance();
          }

          seriesColor = false;
          seriesCount = 0;
          offset++;
          invertedBitCount++;
        } else {

          if (seriesColor == color) {
            seriesCount++;
          } else {
            seriesCount = 1;
            seriesColor = color;
          }

          correctedBits[i * codewordSize + j - offset] = color;

        }

        flag >>>= 1;
      }
    }

    return correctedBits;
  }

  /**
   * Gets the array of bits from an Aztec Code matrix
   *
   * @return the array of bits
   * @throws FormatException if the matrix is not a valid aztec code
   */
  private boolean[] extractBits(BitMatrix matrix) throws FormatException {

    boolean[] rawbits;
    if (ddata.isCompact()) {
      if (ddata.getNbLayers() > NB_BITS_COMPACT.length) {
        throw FormatException.getFormatInstance();
      }
      rawbits = new boolean[NB_BITS_COMPACT[ddata.getNbLayers()]];
      numCodewords = NB_DATABLOCK_COMPACT[ddata.getNbLayers()];
    } else {
      if (ddata.getNbLayers() > NB_BITS.length) {
        throw FormatException.getFormatInstance();
      }
      rawbits = new boolean[NB_BITS[ddata.getNbLayers()]];
      numCodewords = NB_DATABLOCK[ddata.getNbLayers()];
    }

    int layer = ddata.getNbLayers();
    int size = matrix.getHeight();
    int rawbitsOffset = 0;
    int matrixOffset = 0;

    while (layer != 0) {

      int flip = 0;
      for (int i = 0; i < 2 * size - 4; i++) {
        rawbits[rawbitsOffset + i] = matrix.get(matrixOffset + flip, matrixOffset + i / 2);
        rawbits[rawbitsOffset + 2 * size - 4 + i] = matrix.get(matrixOffset + i / 2, matrixOffset + size - 1 - flip);
        flip = (flip + 1) % 2;
      }

      flip = 0;
      for (int i = 2 * size + 1; i > 5; i--) {
        rawbits[rawbitsOffset + 4 * size - 8 + (2 * size - i) + 1] =
            matrix.get(matrixOffset + size - 1 - flip, matrixOffset + i / 2 - 1);
        rawbits[rawbitsOffset + 6 * size - 12 + (2 * size - i) + 1] =
            matrix.get(matrixOffset + i / 2 - 1, matrixOffset + flip);
        flip = (flip + 1) % 2;
      }

      matrixOffset += 2;
      rawbitsOffset += 8 * size - 16;
      layer--;
      size -= 4;
    }

    return rawbits;
  }

  /**
   * Transforms an Aztec code matrix by removing the control dashed lines
   */
  private static BitMatrix removeDashedLines(BitMatrix matrix) {
    int nbDashed = 1 + 2 * ((matrix.getWidth() - 1) / 2 / 16);
    BitMatrix newMatrix = new BitMatrix(matrix.getWidth() - nbDashed, matrix.getHeight() - nbDashed);

    int nx = 0;

    for (int x = 0; x < matrix.getWidth(); x++) {

      if ((matrix.getWidth() / 2 - x) % 16 == 0) {
        continue;
      }

      int ny = 0;
      for (int y = 0; y < matrix.getHeight(); y++) {

        if ((matrix.getWidth() / 2 - y) % 16 == 0) {
          continue;
        }

        if (matrix.get(x, y)) {
          newMatrix.set(nx, ny);
        }
        ny++;
      }
      nx++;
    }

    return newMatrix;
  }

  /**
   * Reads a code of given length and at given index in an array of bits
   */
  private static int readCode(boolean[] rawbits, int startIndex, int length) {
    int res = 0;

    for (int i = startIndex; i < startIndex + length; i++) {
      res <<= 1;
      if (rawbits[i]) {
        res++;
      }
    }

    return res;
  }

}
