/*
 * Copyright 2009 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 ZXing.Common;
using ZXing.Common.Detector;

namespace ZXing.PDF417.Internal
{
   /// <summary>
   /// <p>Encapsulates logic that can detect a PDF417 Code in an image, even if the
   /// PDF417 Code is rotated or skewed, or partially obscured.</p>
   ///
   /// <author>SITA Lab ([email protected])</author>
   /// <author>[email protected] (Daniel Switkin)</author>
   /// </summary>
   public sealed class Detector
   {
      private const int INTEGER_MATH_SHIFT = 8;
      private const int PATTERN_MATCH_RESULT_SCALE_FACTOR = 1 << INTEGER_MATH_SHIFT;
      private const int MAX_AVG_VARIANCE = (int)(PATTERN_MATCH_RESULT_SCALE_FACTOR * 0.42f);
      private const int MAX_INDIVIDUAL_VARIANCE = (int)(PATTERN_MATCH_RESULT_SCALE_FACTOR * 0.8f);
      private const int SKEW_THRESHOLD = 3;

      // B S B S B S B S Bar/Space pattern
      // 11111111 0 1 0 1 0 1 000
      private static readonly int[] START_PATTERN = { 8, 1, 1, 1, 1, 1, 1, 3 };

      // 11111111 0 1 0 1 0 1 000
      private static readonly int[] START_PATTERN_REVERSE = { 3, 1, 1, 1, 1, 1, 1, 8 };

      // 1111111 0 1 000 1 0 1 00 1
      private static readonly int[] STOP_PATTERN = { 7, 1, 1, 3, 1, 1, 1, 2, 1 };

      // B S B S B S B S B Bar/Space pattern
      // 1111111 0 1 000 1 0 1 00 1
      private static readonly int[] STOP_PATTERN_REVERSE = { 1, 2, 1, 1, 1, 3, 1, 1, 7 };

      private readonly BinaryBitmap image;

      /// <summary>
      /// Initializes a new instance of the <see cref="Detector"/> class.
      /// </summary>
      /// <param name="image">The image.</param>
      public Detector(BinaryBitmap image)
      {
         this.image = image;
      }

      /// <summary>
      /// <p>Detects a PDF417 Code in an image, simply.</p>
      ///
      /// <returns><see cref="DetectorResult" />encapsulating results of detecting a PDF417 Code
      /// </summary>
      public DetectorResult detect()
      {
         return detect(null);
      }

      /// <summary>
      /// <p>Detects a PDF417 Code in an image. Only checks 0 and 180 degree rotations.</p>
      ///
      /// <param name="hints">optional hints to detector</param>
      /// <returns><see cref="DetectorResult" />encapsulating results of detecting a PDF417 Code
      /// </summary>
      public DetectorResult detect(IDictionary<DecodeHintType, object> hints)
      {
         // Fetch the 1 bit matrix once up front.
         BitMatrix matrix = image.BlackMatrix;

         bool tryHarder = hints != null && hints.ContainsKey(DecodeHintType.TRY_HARDER);
         var resultPointCallback = hints == null || !hints.ContainsKey(DecodeHintType.NEED_RESULT_POINT_CALLBACK) ? null : (ResultPointCallback)hints[DecodeHintType.NEED_RESULT_POINT_CALLBACK];

         // Try to find the vertices assuming the image is upright.
         ResultPoint[] vertices = findVertices(matrix, tryHarder);
         if (vertices == null)
         {
            // Maybe the image is rotated 180 degrees?
            vertices = findVertices180(matrix, tryHarder);
            if (vertices != null)
            {
               correctCodeWordVertices(vertices, true);
            }
         }
         else
         {
            correctCodeWordVertices(vertices, false);
         }

         if (vertices == null)
         {
            return null;
         }

         float moduleWidth = computeModuleWidth(vertices);
         if (moduleWidth < 1.0f)
         {
            return null;
         }

         int dimension = computeDimension(vertices[4], vertices[6],
             vertices[5], vertices[7], moduleWidth);
         if (dimension < 1)
         {
            return null;
         }

         int ydimension = computeYDimension(vertices[4], vertices[6], vertices[5], vertices[7], moduleWidth);
         ydimension = ydimension > dimension ? ydimension : dimension;

         // Deskew and sample image.
         BitMatrix bits = sampleGrid(matrix, vertices[4], vertices[5], vertices[6], vertices[7], dimension, ydimension);
         if (resultPointCallback != null)
         {
            resultPointCallback(vertices[5]);
            resultPointCallback(vertices[4]);
            resultPointCallback(vertices[6]);
            resultPointCallback(vertices[7]);
         }
         return new DetectorResult(bits, new [] { vertices[5], vertices[4], vertices[6], vertices[7] });
      }

      /// <summary>
      /// Locate the vertices and the codewords area of a black blob using the Start
      /// and Stop patterns as locators.
      /// <param name="matrix">the scanned barcode image.</param>
      /// <returns>an array containing the vertices:</returns>
      ///           vertices[0] x, y top left barcode
      ///           vertices[1] x, y bottom left barcode
      ///           vertices[2] x, y top right barcode
      ///           vertices[3] x, y bottom right barcode
      ///           vertices[4] x, y top left codeword area
      ///           vertices[5] x, y bottom left codeword area
      ///           vertices[6] x, y top right codeword area
      ///           vertices[7] x, y bottom right codeword area
      /// </summary>
      private static ResultPoint[] findVertices(BitMatrix matrix, bool tryHarder)
      {
         int height = matrix.Height;
         int width = matrix.Width;

         ResultPoint[] result = new ResultPoint[8];
         bool found = false;

         int[] counters = new int[START_PATTERN.Length];

         int rowStep = Math.Max(1, height >> (tryHarder ? 9 : 7));

         // Top Left
         for (int i = 0; i < height; i += rowStep)
         {
            int[] loc = findGuardPattern(matrix, 0, i, width, false, START_PATTERN, counters);
            if (loc != null)
            {
               result[0] = new ResultPoint(loc[0], i);
               result[4] = new ResultPoint(loc[1], i);
               found = true;
               break;
            }
         }
         // Bottom left
         if (found)
         { // Found the Top Left vertex
            found = false;
            for (int i = height - 1; i > 0; i -= rowStep)
            {
               int[] loc = findGuardPattern(matrix, 0, i, width, false, START_PATTERN, counters);
               if (loc != null)
               {
                  result[1] = new ResultPoint(loc[0], i);
                  result[5] = new ResultPoint(loc[1], i);
                  found = true;
                  break;
               }
            }
         }

         counters = new int[STOP_PATTERN.Length];

         // Top right
         if (found)
         { // Found the Bottom Left vertex
            found = false;
            for (int i = 0; i < height; i += rowStep)
            {
               int[] loc = findGuardPattern(matrix, 0, i, width, false, STOP_PATTERN, counters);
               if (loc != null)
               {
                  result[2] = new ResultPoint(loc[1], i);
                  result[6] = new ResultPoint(loc[0], i);
                  found = true;
                  break;
               }
            }
         }
         // Bottom right
         if (found)
         { // Found the Top right vertex
            found = false;
            for (int i = height - 1; i > 0; i -= rowStep)
            {
               int[] loc = findGuardPattern(matrix, 0, i, width, false, STOP_PATTERN, counters);
               if (loc != null)
               {
                  result[3] = new ResultPoint(loc[1], i);
                  result[7] = new ResultPoint(loc[0], i);
                  found = true;
                  break;
               }
            }
         }
         return found ? result : null;
      }

      /// <summary>
      /// Locate the vertices and the codewords area of a black blob using the Start
      /// and Stop patterns as locators. This assumes that the image is rotated 180
      /// degrees and if it locates the start and stop patterns at it will re-map
      /// the vertices for a 0 degree rotation.
      /// TODO: Change assumption about barcode location.
      /// <param name="matrix">the scanned barcode image.</param>
      /// <returns>an array containing the vertices:</returns>
      ///           vertices[0] x, y top left barcode
      ///           vertices[1] x, y bottom left barcode
      ///           vertices[2] x, y top right barcode
      ///           vertices[3] x, y bottom right barcode
      ///           vertices[4] x, y top left codeword area
      ///           vertices[5] x, y bottom left codeword area
      ///           vertices[6] x, y top right codeword area
      ///           vertices[7] x, y bottom right codeword area
      /// </summary>
      private static ResultPoint[] findVertices180(BitMatrix matrix, bool tryHarder)
      {
         int height = matrix.Height;
         int width = matrix.Width;
         int halfWidth = width >> 1;

         ResultPoint[] result = new ResultPoint[8];
         bool found = false;

         int[] counters = new int[START_PATTERN_REVERSE.Length];

         int rowStep = Math.Max(1, height >> (tryHarder ? 9 : 7));

         // Top Left
         for (int i = height - 1; i > 0; i -= rowStep)
         {
            int[] loc = findGuardPattern(matrix, halfWidth, i, halfWidth, true, START_PATTERN_REVERSE, counters);
            if (loc != null)
            {
               result[0] = new ResultPoint(loc[1], i);
               result[4] = new ResultPoint(loc[0], i);
               found = true;
               break;
            }
         }
         // Bottom Left
         if (found)
         { // Found the Top Left vertex
            found = false;
            for (int i = 0; i < height; i += rowStep)
            {
               int[] loc = findGuardPattern(matrix, halfWidth, i, halfWidth, true, START_PATTERN_REVERSE, counters);
               if (loc != null)
               {
                  result[1] = new ResultPoint(loc[1], i);
                  result[5] = new ResultPoint(loc[0], i);
                  found = true;
                  break;
               }
            }
         }

         counters = new int[STOP_PATTERN_REVERSE.Length];

         // Top Right
         if (found)
         { // Found the Bottom Left vertex
            found = false;
            for (int i = height - 1; i > 0; i -= rowStep)
            {
               int[] loc = findGuardPattern(matrix, 0, i, halfWidth, false, STOP_PATTERN_REVERSE, counters);
               if (loc != null)
               {
                  result[2] = new ResultPoint(loc[0], i);
                  result[6] = new ResultPoint(loc[1], i);
                  found = true;
                  break;
               }
            }
         }
         // Bottom Right
         if (found)
         { // Found the Top Right vertex
            found = false;
            for (int i = 0; i < height; i += rowStep)
            {
               int[] loc = findGuardPattern(matrix, 0, i, halfWidth, false, STOP_PATTERN_REVERSE, counters);
               if (loc != null)
               {
                  result[3] = new ResultPoint(loc[0], i);
                  result[7] = new ResultPoint(loc[1], i);
                  found = true;
                  break;
               }
            }
         }
         return found ? result : null;
      }

      /// <summary>
      /// Because we scan horizontally to detect the start and stop patterns, the vertical component of
      /// the codeword coordinates will be slightly wrong if there is any skew or rotation in the image.
      /// This method moves those points back onto the edges of the theoretically perfect bounding
      /// quadrilateral if needed.
      ///
      /// <param name="vertices">The eight vertices located by findVertices().</param>
      /// </summary>
      private static void correctCodeWordVertices(ResultPoint[] vertices, bool upsideDown)
      {
         float v0x = vertices[0].X;
         float v0y = vertices[0].Y;
         float v2x = vertices[2].X;
         float v2y = vertices[2].Y;
         float v4x = vertices[4].X;
         float v4y = vertices[4].Y;
         float v6x = vertices[6].X;
         float v6y = vertices[6].Y;

         float skew = v4y - v6y;
         if (upsideDown)
         {
            skew = -skew;
         }
         if (skew > SKEW_THRESHOLD)
         {
            // Fix v4
            float deltax = v6x - v0x;
            float deltay = v6y - v0y;
            float delta2 = deltax*deltax + deltay*deltay;
            float correction = (v4x - v0x)*deltax/delta2;
            vertices[4] = new ResultPoint(v0x + correction*deltax, v0y + correction*deltay);
         }
         else if (-skew > SKEW_THRESHOLD)
         {
            // Fix v6
            float deltax = v2x - v4x;
            float deltay = v2y - v4y;
            float delta2 = deltax*deltax + deltay*deltay;
            float correction = (v2x - v6x)*deltax/delta2;
            vertices[6] = new ResultPoint(v2x - correction*deltax, v2y - correction*deltay);
         }

         float v1x = vertices[1].X;
         float v1y = vertices[1].Y;
         float v3x = vertices[3].X;
         float v3y = vertices[3].Y;
         float v5x = vertices[5].X;
         float v5y = vertices[5].Y;
         float v7x = vertices[7].X;
         float v7y = vertices[7].Y;

         skew = v7y - v5y;
         if (upsideDown)
         {
            skew = -skew;
         }
         if (skew > SKEW_THRESHOLD)
         {
            // Fix v5
            float deltax = v7x - v1x;
            float deltay = v7y - v1y;
            float delta2 = deltax*deltax + deltay*deltay;
            float correction = (v5x - v1x)*deltax/delta2;
            vertices[5] = new ResultPoint(v1x + correction*deltax, v1y + correction*deltay);
         }
         else if (-skew > SKEW_THRESHOLD)
         {
            // Fix v7
            float deltax = v3x - v5x;
            float deltay = v3y - v5y;
            float delta2 = deltax*deltax + deltay*deltay;
            float correction = (v3x - v7x)*deltax/delta2;
            vertices[7] = new ResultPoint(v3x - correction*deltax, v3y - correction*deltay);
         }
      }

      /// <summary>
      /// <p>Estimates module size (pixels in a module) based on the Start and End
      /// finder patterns.</p>
      ///
      /// <param name="vertices">an array of vertices:</param>
      ///           vertices[0] x, y top left barcode
      ///           vertices[1] x, y bottom left barcode
      ///           vertices[2] x, y top right barcode
      ///           vertices[3] x, y bottom right barcode
      ///           vertices[4] x, y top left codeword area
      ///           vertices[5] x, y bottom left codeword area
      ///           vertices[6] x, y top right codeword area
      ///           vertices[7] x, y bottom right codeword area
      /// <returns>the module size.</returns>
      /// </summary>
      private static float computeModuleWidth(ResultPoint[] vertices)
      {
         float pixels1 = ResultPoint.distance(vertices[0], vertices[4]);
         float pixels2 = ResultPoint.distance(vertices[1], vertices[5]);
         float moduleWidth1 = (pixels1 + pixels2) / (17 * 2.0f);
         float pixels3 = ResultPoint.distance(vertices[6], vertices[2]);
         float pixels4 = ResultPoint.distance(vertices[7], vertices[3]);
         float moduleWidth2 = (pixels3 + pixels4) / (18 * 2.0f);
         return (moduleWidth1 + moduleWidth2) / 2.0f;
      }

      /// <summary>
      /// Computes the dimension (number of modules in a row) of the PDF417 Code
      /// based on vertices of the codeword area and estimated module size.
      ///
      /// <param name="topLeft">of codeword area</param>
      /// <param name="topRight">of codeword area</param>
      /// <param name="bottomLeft">of codeword area</param>
      /// <param name="bottomRight">of codeword are</param>
      /// <param name="moduleWidth">estimated module size</param>
      /// <returns>the number of modules in a row.</returns>
      /// </summary>
      private static int computeDimension(ResultPoint topLeft,
                                          ResultPoint topRight,
                                          ResultPoint bottomLeft,
                                          ResultPoint bottomRight,
                                          float moduleWidth)
      {
         int topRowDimension = MathUtils.round(ResultPoint.distance(topLeft, topRight) / moduleWidth);
         int bottomRowDimension = MathUtils.round(ResultPoint.distance(bottomLeft, bottomRight) / moduleWidth);
         return ((((topRowDimension + bottomRowDimension) >> 1) + 8) / 17) * 17;
      }


      /// <summary>
      /// Computes the y dimension (number of modules in a column) of the PDF417 Code
      /// based on vertices of the codeword area and estimated module size.
      /// </summary>
      /// <param name="topLeft">of codeword area</param>
      /// <param name="topRight">of codeword area</param>
      /// <param name="bottomLeft">of codeword area</param>
      /// <param name="bottomRight">of codeword are</param>
      /// <param name="moduleWidth">estimated module size</param>
      /// <returns>the number of modules in a row.</returns>
      private static int computeYDimension(ResultPoint topLeft,
                                          ResultPoint topRight,
                                          ResultPoint bottomLeft,
                                          ResultPoint bottomRight,
                                          float moduleWidth)
      {
         int leftColumnDimension = MathUtils.round(ResultPoint.distance(topLeft, bottomLeft) / moduleWidth);
         int rightColumnDimension = MathUtils.round(ResultPoint.distance(topRight, bottomRight) / moduleWidth);
         return (leftColumnDimension + rightColumnDimension) >> 1;
      }

      private static BitMatrix sampleGrid(BitMatrix matrix,
                                          ResultPoint topLeft,
                                          ResultPoint bottomLeft,
                                          ResultPoint topRight,
                                          ResultPoint bottomRight,
                                          int xdimension,
                                          int ydimension)
      {

         // Note that unlike the QR Code sampler, we didn't find the center of modules, but the
         // very corners. So there is no 0.5f here; 0.0f is right.
         GridSampler sampler = GridSampler.Instance;

         return sampler.sampleGrid(
            matrix,
            xdimension, ydimension,
            0.0f, // p1ToX
            0.0f, // p1ToY
            xdimension, // p2ToX
            0.0f, // p2ToY
            xdimension, // p3ToX
            ydimension, // p3ToY
            0.0f, // p4ToX
            ydimension, // p4ToY
            topLeft.X, // p1FromX
            topLeft.Y, // p1FromY
            topRight.X, // p2FromX
            topRight.Y, // p2FromY
            bottomRight.X, // p3FromX
            bottomRight.Y, // p3FromY
            bottomLeft.X, // p4FromX
            bottomLeft.Y); // p4FromY
      }

      /// <summary>
      /// <param name="matrix">row of black/white values to search</param>
      /// <param name="column">x position to start search</param>
      /// <param name="row">y position to start search</param>
      /// <param name="width">the number of pixels to search on this row</param>
      /// <param name="pattern">pattern of counts of number of black and white pixels that are</param>
      ///                 being searched for as a pattern
      /// <param name="counters">array of counters, as long as pattern, to re-use </param>
      /// <returns>start/end horizontal offset of guard pattern, as an array of two ints.</returns>
      /// </summary>
      private static int[] findGuardPattern(BitMatrix matrix,
                                            int column,
                                            int row,
                                            int width,
                                            bool whiteFirst,
                                            int[] pattern,
                                            int[] counters)
      {
         for (int i = 0; i < counters.Length; i++)
            counters[i] = 0;
         int patternLength = pattern.Length;
         bool isWhite = whiteFirst;

         int counterPosition = 0;
         int patternStart = column;
         for (int x = column; x < column + width; x++)
         {
            bool pixel = matrix[x, row];
            if (pixel ^ isWhite)
            {
               counters[counterPosition]++;
            }
            else
            {
               if (counterPosition == patternLength - 1)
               {
                  if (patternMatchVariance(counters, pattern, MAX_INDIVIDUAL_VARIANCE) < MAX_AVG_VARIANCE)
                  {
                     return new int[] { patternStart, x };
                  }
                  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;
      }

      /// <summary>
      /// Determines how closely a set of observed counts of runs of black/white
      /// values matches a given target pattern. This is reported as the ratio of
      /// the total variance from the expected pattern proportions across all
      /// pattern elements, to the length of the pattern.
      ///
      /// <param name="counters">observed counters</param>
      /// <param name="pattern">expected pattern</param>
      /// <param name="maxIndividualVariance">The most any counter can differ before we give up</param>
      /// <returns>ratio of total variance between counters and pattern compared to</returns>
      ///         total pattern size, where the ratio has been multiplied by 256.
      ///         So, 0 means no variance (perfect match); 256 means the total
      ///         variance between counters and patterns equals the pattern length,
      ///         higher values mean even more variance
      /// </summary>
      private static int patternMatchVariance(int[] counters, int[] pattern, int maxIndividualVariance)
      {
         int numCounters = counters.Length;
         int total = 0;
         int patternLength = 0;
         for (int i = 0; i < numCounters; i++)
         {
            total += counters[i];
            patternLength += pattern[i];
         }
         if (total < patternLength)
         {
            // If we don't even have one pixel per unit of bar width, assume this
            // is too small to reliably match, so fail:
            return Int32.MaxValue;
         }
         // We're going to fake floating-point math in integers. We just need to use more bits.
         // Scale up patternLength so that intermediate values below like scaledCounter will have
         // more "significant digits".
         int unitBarWidth = (total << INTEGER_MATH_SHIFT) / patternLength;
         maxIndividualVariance = (maxIndividualVariance * unitBarWidth) >> 8;

         int totalVariance = 0;
         for (int x = 0; x < numCounters; x++)
         {
            int counter = counters[x] << INTEGER_MATH_SHIFT;
            int scaledPattern = pattern[x] * unitBarWidth;
            int variance = counter > scaledPattern ? counter - scaledPattern : scaledPattern - counter;
            if (variance > maxIndividualVariance)
            {
               return Int32.MaxValue;
            }
            totalVariance += variance;
         }
         return totalVariance / total;
      }

   }
}