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

namespace ZXing.OneD
{
   /// <summary>
   /// Decodes MSI barcodes.
   /// </summary>
   public sealed class MSIReader : OneDReader
   {
      internal static String ALPHABET_STRING = "0123456789";
      private static readonly char[] ALPHABET = ALPHABET_STRING.ToCharArray();

      /// <summary>
      /// These represent the encodings of characters, as patterns of wide and narrow bars.
      /// The 9 least-significant bits of each int correspond to the pattern of wide and narrow,
      /// with 1s representing "wide" and 0s representing narrow.
      /// </summary>
      internal static int[] CHARACTER_ENCODINGS = {
                                                     0x924, 0x926, 0x934, 0x936, 0x9A4, 0x9A6, 0x9B4, 0x9B6, 0xD24, 0xD26 // 0-9
                                                  };

      private const int START_ENCODING = 0x06;
      private const int END_ENCODING = 0x09;

      private readonly bool usingCheckDigit;
      private readonly StringBuilder decodeRowResult;
      private readonly int[] counters;
      private int averageCounterWidth;

      /// <summary>
      /// Creates a reader that assumes all encoded data is data, and does not treat the final
      /// character as a check digit.
      /// </summary>
      public MSIReader()
         : this(false)
      {
      }

      /// <summary>
      /// Creates a reader that can be configured to check the last character as a check digit,
      /// </summary>
      /// <param name="usingCheckDigit">if true, treat the last data character as a check digit, not
      /// data, and verify that the checksum passes.</param>
      public MSIReader(bool usingCheckDigit)
      {
         this.usingCheckDigit = usingCheckDigit;
         decodeRowResult = new StringBuilder(20);
         counters = new int[8];
      }

      /// <summary>
      ///   <p>Attempts to decode a one-dimensional barcode format given a single row of
      /// an image.</p>
      /// </summary>
      /// <param name="rowNumber">row number from top of the row</param>
      /// <param name="row">the black/white pixel data of the row</param>
      /// <param name="hints">decode hints</param>
      /// <returns><see cref="Result"/>containing encoded string and start/end of barcode</returns>
      override public Result decodeRow(int rowNumber, BitArray row, IDictionary<DecodeHintType, object> hints)
      {
         for (var index = 0; index < counters.Length; index++)
            counters[index] = 0;
         decodeRowResult.Length = 0;

         int[] start = findStartPattern(row, counters);
         if (start == null)
            return null;

         // Read off white space    
         int nextStart = row.getNextSet(start[1]);

         char decodedChar;
         int lastStart = nextStart;
         int pattern;
         do
         {
            if (!recordPattern(row, nextStart, counters, 8))
            {
               // not enough bars for a number but perhaps enough for the end pattern
               var endPattern = findEndPattern(row, nextStart, counters);
               if (endPattern == null)
                  return null;
               lastStart = nextStart;
               nextStart = endPattern[1];
               break;
            }
            pattern = toPattern(counters, 8);
            if (!patternToChar(pattern, out decodedChar))
            {
               // pattern doesn't result in an encoded number
               // but it could be the end pattern followed by some black areas
               var endPattern = findEndPattern(row, nextStart, counters);
               if (endPattern == null)
                  return null;
               lastStart = nextStart;
               nextStart = endPattern[1];
               break;
            }
            decodeRowResult.Append(decodedChar);
            lastStart = nextStart;
            foreach (int counter in counters)
            {
               nextStart += counter;
            }
            // Read off white space
            nextStart = row.getNextSet(nextStart);
         } while (decodedChar != '*');
         
         // at least 3 digits to prevent false positives within other kind
         // of codes like PDF417
         if (decodeRowResult.Length < 3)
         {
            return null;
         }

         var rawBytes = Encoding.UTF8.GetBytes(decodeRowResult.ToString());
         var resultString = decodeRowResult.ToString();

         if (usingCheckDigit)
         {
            var resultStringWithoutChecksum = resultString.Substring(0, resultString.Length - 1);
            int checkSum = CalculateChecksumLuhn(resultStringWithoutChecksum);
            if ((char)(checkSum + 48) != resultString[resultStringWithoutChecksum.Length])
            {
               return null;
            }
         }

         float left = (float)(start[1] + start[0]) / 2.0f;
         float right = (float)(nextStart + lastStart) / 2.0f;

         var resultPointCallback = hints == null || !hints.ContainsKey(DecodeHintType.NEED_RESULT_POINT_CALLBACK)
                                      ? null
                                      : (ResultPointCallback)hints[DecodeHintType.NEED_RESULT_POINT_CALLBACK];
         if (resultPointCallback != null)
         {
            resultPointCallback(new ResultPoint(left, rowNumber));
            resultPointCallback(new ResultPoint(right, rowNumber));
         }

         return new Result(
            resultString,
            rawBytes,
            new[]
               {
                  new ResultPoint(left, rowNumber),
                  new ResultPoint(right, rowNumber)
               },
            BarcodeFormat.MSI);
      }

      private int[] findStartPattern(BitArray row, int[] counters)
      {
         const int patternLength = 2;

         int width = row.Size;
         int rowOffset = row.getNextSet(0);

         int counterPosition = 0;
         int patternStart = rowOffset;
         bool isWhite = false;

         counters[0] = 0;
         counters[1] = 0;
         for (int i = rowOffset; i < width; i++)
         {
            if (row[i] ^ isWhite)
            {
               counters[counterPosition]++;
            }
            else
            {
               if (counterPosition == patternLength - 1)
               {
                  // narrow and wide areas should be as near as possible to factor 2
                  // lets say we will check 1.5 <= factor <= 5
                  var factorNarrowToWide = ((float)counters[0]) / ((float)counters[1]);
                  if (factorNarrowToWide >= 1.5 && factorNarrowToWide <= 5)
                  {
                     calculateAverageCounterWidth(counters, patternLength);
                     if (toPattern(counters, patternLength) == START_ENCODING)
                     {
                        // Look for whitespace before start pattern, >= 50% of width of start pattern
                        if (row.isRange(Math.Max(0, patternStart - ((i - patternStart) >> 1)), patternStart, false))
                        {
                           return new int[] {patternStart, i};
                        }
                     }
                  }
                  patternStart += counters[0] + counters[1];
                  Array.Copy(counters, 2, counters, 0, patternLength - 2);
                  counters[patternLength - 2] = 0;
                  counters[patternLength - 1] = 0;
                  counterPosition--;
               }
               else
               {
                  counterPosition++;
               }
               counters[counterPosition] = 1;
               isWhite = !isWhite;
            }
         }
         return null;
      }

      private int[] findEndPattern(BitArray row, int rowOffset, int[] counters)
      {
         const int patternLength = 3;

         int width = row.Size;

         int counterPosition = 0;
         int patternStart = rowOffset;
         bool isWhite = false;

         counters[0] = 0;
         counters[1] = 0;
         counters[2] = 0;
         for (int i = rowOffset; i < width; i++)
         {
            if (row[i] ^ isWhite)
            {
               counters[counterPosition]++;
            }
            else
            {
               if (counterPosition == patternLength - 1)
               {
                  var factorNarrowToWide = ((float)counters[1]) / ((float)counters[0]);
                  if (factorNarrowToWide >= 1.5 && factorNarrowToWide <= 5)
                  {
                     if (toPattern(counters, patternLength) == END_ENCODING)
                     {
                        // Look for whitespace after end pattern, >= 50% of width of end pattern
                        var minEndOfWhite = Math.Min(row.Size - 1, i + ((i - patternStart) >> 1));
                        if (row.isRange(i, minEndOfWhite, false))
                        {
                           return new int[] {patternStart, i};
                        }
                     }
                  }
                  return null;
               }
               counterPosition++;
               counters[counterPosition] = 1;
               isWhite = !isWhite;
            }
         }
         return null;
      }

      private void calculateAverageCounterWidth(int[] counters, int patternLength)
      {
         // look for the minimum and the maximum width of the bars
         // there are only two sizes for MSI barcodes
         // all numbers are encoded as a chain of the pattern 100 and 110
         // the complete pattern of one number always starts with 1 or 11 (black bar(s))
         int minCounter = Int32.MaxValue;
         int maxCounter = 0;
         for (var index = 0; index < patternLength; index++)
         {
            var counter = counters[index];
            if (counter < minCounter)
            {
               minCounter = counter;
            }
            if (counter > maxCounter)
            {
               maxCounter = counter;
            }
         }
         // calculate the average of the minimum and maximum width
         // using some bit shift to get a higher resolution without floating point arithmetic
         averageCounterWidth = ((maxCounter << 8) + (minCounter << 8)) / 2;
      }

      private int toPattern(int[] counters, int patternLength)
      {
         // calculating the encoded value from the pattern
         int pattern = 0;
         int bit = 1;
         int doubleBit = 3;
         for (var index = 0; index < patternLength; index++)
         {
            var counter = counters[index];
            if ((counter << 8) < averageCounterWidth)
            {
               pattern = (pattern << 1) | bit;
            }
            else
            {
               pattern = (pattern << 2) | doubleBit;
            }
            bit = bit ^ 1;
            doubleBit = doubleBit ^ 3;
         }

         return pattern;
      }

      private static bool patternToChar(int pattern, out char c)
      {
         for (int i = 0; i < CHARACTER_ENCODINGS.Length; i++)
         {
            if (CHARACTER_ENCODINGS[i] == pattern)
            {
               c = ALPHABET[i];
               return true;
            }
         }
         c = '*';
         return false;
      }

      private static readonly int[] doubleAndCrossSum = new [] { 0, 2, 4, 6, 8, 1, 3, 5, 7, 9 };

      private static int CalculateChecksumLuhn(string number)
      {
         var checksum = 0;

         for (var index = number.Length - 2; index >= 0; index -= 2)
         {
            var digit = number[index] - 48;
            checksum += digit;
         }
         for (var index = number.Length - 1; index >= 0; index -= 2)
         {
            var digit = doubleAndCrossSum[number[index] - 48];
            checksum += digit;
         }

         return (10 - (checksum % 10)) % 10;
      }
   }
}