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

using System;
using System.Collections.Generic;
using System.Text;

using ZXing.Common;
using ZXing.Common.ReedSolomon;

namespace ZXing.Aztec.Internal
{
   /// <summary>
   /// The main class which implements Aztec Code decoding -- as opposed to locating and extracting
   /// the Aztec Code from an image.
   /// </summary>
   /// <author>David Olivier</author>
   public sealed class Decoder
   {
      private enum Table
      {
         UPPER,
         LOWER,
         MIXED,
         DIGIT,
         PUNCT,
         BINARY
      }

      private static readonly 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 readonly 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 readonly String[] MIXED_TABLE =
         {
            "CTRL_PS", " ", "\x1", "\x2", "\x3", "\x4", "\x5", "\x6", "\x7", "\b", "\t", "\n",
            "\xD", "\f", "\r", "\x21", "\x22", "\x23", "\x24", "\x25", "@", "\\", "^", "_",
            "`", "|", "~", "\xB1", "CTRL_LL", "CTRL_UL", "CTRL_PL", "CTRL_BS"
         };

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

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

      private static readonly IDictionary<Table, String[]> codeTables = new Dictionary<Table, String[]>
         {
            {Table.UPPER, UPPER_TABLE},
            {Table.LOWER, LOWER_TABLE},
            {Table.MIXED, MIXED_TABLE},
            {Table.PUNCT, PUNCT_TABLE},
            {Table.DIGIT, DIGIT_TABLE},
            {Table.BINARY, null}
         };

      private static readonly IDictionary<char, Table> codeTableMap = new Dictionary<char, Table>
         {
            {'U', Table.UPPER},
            {'L', Table.LOWER},
            {'M', Table.MIXED},
            {'P', Table.PUNCT},
            {'D', Table.DIGIT},
            {'B', Table.BINARY}
         };

      private AztecDetectorResult ddata;

      /// <summary>
      /// Decodes the specified detector result.
      /// </summary>
      /// <param name="detectorResult">The detector result.</param>
      /// <returns></returns>
      public DecoderResult decode(AztecDetectorResult detectorResult)
      {
         ddata = detectorResult;
         BitMatrix matrix = detectorResult.Bits;
         bool[] rawbits = extractBits(matrix);
         if (rawbits == null)
            return null;

         bool[] correctedBits = correctBits(rawbits);
         if (correctedBits == null)
            return null;

         String result = getEncodedData(correctedBits);
         if (result == null)
            return null;

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

      // This method is used for testing the high-level encoder
      public static String highLevelDecode(bool[] correctedBits)
      {
         return getEncodedData(correctedBits);
      }

      /// <summary>
      /// Returns the string encoded in the aztec code bits
      /// </summary>
      /// <param name="correctedBits">The corrected bits.</param>
      /// <returns>the decoded string</returns>
      private static String getEncodedData(bool[] correctedBits)
      {
         var endIndex = correctedBits.Length;
         var latchTable = Table.UPPER; // table most recently latched to
         var shiftTable = Table.UPPER; // table to use for the next read
         var strTable = UPPER_TABLE;
         var result = new StringBuilder(20);
         var index = 0;

         while (index < endIndex)
         {
            if (shiftTable == Table.BINARY)
            {
               if (endIndex - index < 5)
               {
                  break;
               }
               int length = readCode(correctedBits, index, 5);
               index += 5;
               if (length == 0)
               {
                  if (endIndex - index < 11)
                  {
                     break;
                  }
                  length = readCode(correctedBits, index, 11) + 31;
                  index += 11;
               }
               for (int charCount = 0; charCount < length; charCount++)
               {
                  if (endIndex - index < 8)
                  {
                     index = endIndex; // Force outer loop to exit
                     break;
                  }
                  int code = readCode(correctedBits, index, 8);
                  result.Append((char) code);
                  index += 8;
               }
               // Go back to whatever mode we had been in
               shiftTable = latchTable;
               strTable = codeTables[shiftTable];
            }
            else
            {
               int size = shiftTable == Table.DIGIT ? 4 : 5;
               if (endIndex - index < size)
               {
                  break;
               }
               int code = readCode(correctedBits, index, size);
               index += size;
               String str = getCharacter(strTable, code);
               if (str.StartsWith("CTRL_"))
               {
                  // Table changes
                  shiftTable = getTable(str[5]);
                  strTable = codeTables[shiftTable];
                  if (str[6] == 'L')
                  {
                     latchTable = shiftTable;
                  }
               }
               else
               {
                  result.Append(str);
                  // Go back to whatever mode we had been in
                  shiftTable = latchTable;
                  strTable = codeTables[shiftTable];
               }
            }
         }
         return result.ToString();
      }

      /// <summary>
      /// gets the table corresponding to the char passed
      /// </summary>
      /// <param name="t">The t.</param>
      /// <returns></returns>
      private static Table getTable(char t)
      {
         if (!codeTableMap.ContainsKey(t))
            return codeTableMap['U'];
         return codeTableMap[t];
      }

      /// <summary>
      /// Returns the character (or string) corresponding to the passed code in the given table
      /// </summary>
      /// <param name="table">the table used</param>
      /// <param name="code">the code of the character</param>
      /// <returns></returns>
      private static String getCharacter(String[] table, int code)
      {
         return table[code];
      }

      /// <summary>
      ///Performs RS error correction on an array of bits.
      /// </summary>
      /// <param name="rawbits">The rawbits.</param>
      /// <returns>the corrected array</returns>
      private bool[] correctBits(bool[] rawbits)
      {
         GenericGF gf;
         int codewordSize;

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

         int numDataCodewords = ddata.NbDatablocks;
         int numCodewords = rawbits.Length/codewordSize;
         int offset = rawbits.Length%codewordSize;
         int numECCodewords = numCodewords - numDataCodewords;

         int[] dataWords = new int[numCodewords];
         for (int i = 0; i < numCodewords; i++, offset += codewordSize)
         {
            dataWords[i] = readCode(rawbits, offset, codewordSize);
         }

         var rsDecoder = new ReedSolomonDecoder(gf);
         if (!rsDecoder.decode(dataWords, numECCodewords))
            return null;

         // Now perform the unstuffing operation.
         // First, count how many bits are going to be thrown out as stuffing
         int mask = (1 << codewordSize) - 1;
         int stuffedBits = 0;
         for (int i = 0; i < numDataCodewords; i++)
         {
            int dataWord = dataWords[i];
            if (dataWord == 0 || dataWord == mask)
            {
               return null;
            }
            else if (dataWord == 1 || dataWord == mask - 1)
            {
               stuffedBits++;
            }
         }
         // Now, actually unpack the bits and remove the stuffing
         bool[] correctedBits = new bool[numDataCodewords*codewordSize - stuffedBits];
         int index = 0;
         for (int i = 0; i < numDataCodewords; i++)
         {
            int dataWord = dataWords[i];
            if (dataWord == 1 || dataWord == mask - 1)
            {
               // next codewordSize-1 bits are all zeros or all ones
               SupportClass.Fill(correctedBits, index, index + codewordSize - 1, dataWord > 1);
               index += codewordSize - 1;
            }
            else
            {
               for (int bit = codewordSize - 1; bit >= 0; --bit)
               {
                  correctedBits[index++] = (dataWord & (1 << bit)) != 0;
               }
            }
         }

         if (index != correctedBits.Length)
            return null;

         return correctedBits;
      }

      /// <summary>
      /// Returns the array of bits from an Aztec Code matrix
      /// </summary>
      /// <param name="matrix">The matrix.</param>
      /// <returns>the array of bits</returns>
      private bool[] extractBits(BitMatrix matrix)
      {
         bool compact = ddata.Compact;
         int layers = ddata.NbLayers;
         int baseMatrixSize = compact ? 11 + layers*4 : 14 + layers*4; // not including alignment lines
         int[] alignmentMap = new int[baseMatrixSize];
         bool[] rawbits = new bool[totalBitsInLayer(layers, compact)];

         if (compact)
         {
            for (int i = 0; i < alignmentMap.Length; i++)
            {
               alignmentMap[i] = i;
            }
         }
         else
         {
            int matrixSize = baseMatrixSize + 1 + 2*((baseMatrixSize/2 - 1)/15);
            int origCenter = baseMatrixSize/2;
            int center = matrixSize/2;
            for (int i = 0; i < origCenter; i++)
            {
               int newOffset = i + i/15;
               alignmentMap[origCenter - i - 1] = center - newOffset - 1;
               alignmentMap[origCenter + i] = center + newOffset + 1;
            }
         }
         for (int i = 0, rowOffset = 0; i < layers; i++)
         {
            int rowSize = compact ? (layers - i)*4 + 9 : (layers - i)*4 + 12;
            // The top-left most point of this layer is <low, low> (not including alignment lines)
            int low = i*2;
            // The bottom-right most point of this layer is <high, high> (not including alignment lines)
            int high = baseMatrixSize - 1 - low;
            // We pull bits from the two 2 x rowSize columns and two rowSize x 2 rows
            for (int j = 0; j < rowSize; j++)
            {
               int columnOffset = j*2;
               for (int k = 0; k < 2; k++)
               {
                  // left column
                  rawbits[rowOffset + columnOffset + k] =
                     matrix[alignmentMap[low + k], alignmentMap[low + j]];
                  // bottom row
                  rawbits[rowOffset + 2*rowSize + columnOffset + k] =
                     matrix[alignmentMap[low + j], alignmentMap[high - k]];
                  // right column
                  rawbits[rowOffset + 4*rowSize + columnOffset + k] =
                     matrix[alignmentMap[high - k], alignmentMap[high - j]];
                  // top row
                  rawbits[rowOffset + 6*rowSize + columnOffset + k] =
                     matrix[alignmentMap[high - j], alignmentMap[low + k]];
               }
            }
            rowOffset += rowSize*8;
         }
         return rawbits;
      }

      /// <summary>
      /// Reads a code of given length and at given index in an array of bits
      /// </summary>
      /// <param name="rawbits">The rawbits.</param>
      /// <param name="startIndex">The start index.</param>
      /// <param name="length">The length.</param>
      /// <returns></returns>
      private static int readCode(bool[] rawbits, int startIndex, int length)
      {
         int res = 0;
         for (int i = startIndex; i < startIndex + length; i++)
         {
            res <<= 1;
            if (rawbits[i])
            {
               res++;
            }
         }
         return res;
      }

      private static int totalBitsInLayer(int layers, bool compact)
      {
         return ((compact ? 88 : 112) + 16*layers)*layers;
      }
   }
}