/*
 * Copyright 2007 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.QrCode.Internal;

namespace ZXing.QrCode
{
   /// <summary>
   /// This implementation can detect and decode QR Codes in an image.
   /// <author>Sean Owen</author>
   /// </summary>
   public class QRCodeReader : Reader
   {
      private static readonly ResultPoint[] NO_POINTS = new ResultPoint[0];

      private readonly Decoder decoder = new Decoder();

      /// <summary>
      /// Returns the decoder.
      /// </summary>
      /// <returns></returns>
      protected Decoder getDecoder()
      {
         return decoder;
      }

      /// <summary>
      /// Locates and decodes a QR code in an image.
      ///
      /// <returns>a String representing the content encoded by the QR code</returns>
      /// </summary>
      public Result decode(BinaryBitmap image)
      {
         return decode(image, null);
      }

      /// <summary>
      /// Locates and decodes a barcode in some format within an image. This method also accepts
      /// hints, each possibly associated to some data, which may help the implementation decode.
      /// </summary>
      /// <param name="image">image of barcode to decode</param>
      /// <param name="hints">passed as a <see cref="IDictionary{TKey, TValue}"/> from <see cref="DecodeHintType"/>
      /// to arbitrary data. The
      /// meaning of the data depends upon the hint type. The implementation may or may not do
      /// anything with these hints.</param>
      /// <returns>
      /// String which the barcode encodes
      /// </returns>
      public Result decode(BinaryBitmap image, IDictionary<DecodeHintType, object> hints)
      {
         DecoderResult decoderResult;
         ResultPoint[] points;
         if (image == null || image.BlackMatrix == null)
         {
            // something is wrong with the image
            return null;
         }
         if (hints != null && hints.ContainsKey(DecodeHintType.PURE_BARCODE))
         {
            var bits = extractPureBits(image.BlackMatrix);
            if (bits == null)
               return null;
            decoderResult = decoder.decode(bits, hints);
            points = NO_POINTS;
         }
         else
         {
            var detectorResult = new Detector(image.BlackMatrix).detect(hints);
            if (detectorResult == null)
               return null;
            decoderResult = decoder.decode(detectorResult.Bits, hints);
            points = detectorResult.Points;
         }
         if (decoderResult == null)
            return null;

         // If the code was mirrored: swap the bottom-left and the top-right points.
         var data = decoderResult.Other as QRCodeDecoderMetaData;
         if (data != null)
         {
            data.applyMirroredCorrection(points);
         }

         var result = new Result(decoderResult.Text, decoderResult.RawBytes, points, BarcodeFormat.QR_CODE);
         var byteSegments = decoderResult.ByteSegments;
         if (byteSegments != null)
         {
            result.putMetadata(ResultMetadataType.BYTE_SEGMENTS, byteSegments);
         }
         var ecLevel = decoderResult.ECLevel;
         if (ecLevel != null)
         {
            result.putMetadata(ResultMetadataType.ERROR_CORRECTION_LEVEL, ecLevel);
         }
         if (decoderResult.StructuredAppend)
         {
            result.putMetadata(ResultMetadataType.STRUCTURED_APPEND_SEQUENCE, decoderResult.StructuredAppendSequenceNumber);
            result.putMetadata(ResultMetadataType.STRUCTURED_APPEND_PARITY, decoderResult.StructuredAppendParity);
         }
         return result;
      }

      /// <summary>
      /// Resets any internal state the implementation has after a decode, to prepare it
      /// for reuse.
      /// </summary>
      public void reset()
      {
         // do nothing
      }

      /// <summary>
      /// This method detects a code in a "pure" image -- that is, pure monochrome image
      /// which contains only an unrotated, unskewed, image of a code, with some white border
      /// around it. This is a specialized method that works exceptionally fast in this special
      /// case.
      /// 
      /// <seealso cref="ZXing.Datamatrix.DataMatrixReader.extractPureBits(BitMatrix)" />
      /// </summary>
      private static BitMatrix extractPureBits(BitMatrix image)
      {
         int[] leftTopBlack = image.getTopLeftOnBit();
         int[] rightBottomBlack = image.getBottomRightOnBit();
         if (leftTopBlack == null || rightBottomBlack == null)
         {
            return null;
         }

         float moduleSize;
         if (!QRCodeReader.moduleSize(leftTopBlack, image, out moduleSize))
            return null;

         int top = leftTopBlack[1];
         int bottom = rightBottomBlack[1];
         int left = leftTopBlack[0];
         int right = rightBottomBlack[0];

         // Sanity check!
         if (left >= right || top >= bottom)
         {
            return null;
         }

         if (bottom - top != right - left)
         {
            // Special case, where bottom-right module wasn't black so we found something else in the last row
            // Assume it's a square, so use height as the width
            right = left + (bottom - top);
         }

         int matrixWidth = (int)Math.Round((right - left + 1) / moduleSize);
         int matrixHeight = (int)Math.Round((bottom - top + 1) / moduleSize);
         if (matrixWidth <= 0 || matrixHeight <= 0)
         {
            return null;
         }
         if (matrixHeight != matrixWidth)
         {
            // Only possibly decode square regions
            return null;
         }

         // Push in the "border" by half the module width so that we start
         // sampling in the middle of the module. Just in case the image is a
         // little off, this will help recover.
         int nudge = (int)(moduleSize / 2.0f);
         top += nudge;
         left += nudge;

         // But careful that this does not sample off the edge
         int nudgedTooFarRight = left + (int)((matrixWidth - 1) * moduleSize) - (right - 1);
         if (nudgedTooFarRight > 0)
         {
            if (nudgedTooFarRight > nudge)
            {
               // Neither way fits; abort
               return null;
            }
            left -= nudgedTooFarRight;
         }
         int nudgedTooFarDown = top + (int)((matrixHeight - 1) * moduleSize) - (bottom - 1);
         if (nudgedTooFarDown > 0)
         {
            if (nudgedTooFarDown > nudge)
            {
               // Neither way fits; abort
               return null;
            }
            top -= nudgedTooFarDown;
         }

         // Now just read off the bits
         BitMatrix bits = new BitMatrix(matrixWidth, matrixHeight);
         for (int y = 0; y < matrixHeight; y++)
         {
            int iOffset = top + (int)(y * moduleSize);
            for (int x = 0; x < matrixWidth; x++)
            {
               if (image[left + (int)(x * moduleSize), iOffset])
               {
                  bits[x, y] = true;
               }
            }
         }
         return bits;
      }

      private static bool moduleSize(int[] leftTopBlack, BitMatrix image, out float msize)
      {
         int height = image.Height;
         int width = image.Width;
         int x = leftTopBlack[0];
         int y = leftTopBlack[1];
         bool inBlack = true;
         int transitions = 0;
         while (x < width && y < height)
         {
            if (inBlack != image[x, y])
            {
               if (++transitions == 5)
               {
                  break;
               }
               inBlack = !inBlack;
            }
            x++;
            y++;
         }
         if (x == width || y == height)
         {
            msize = 0.0f;
            return false;
         }
         msize = (x - leftTopBlack[0]) / 7.0f;
         return true;
      }
   }
}