/*
 * Copyright 2013 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.Globalization;
using System.Text;

namespace ZXing.PDF417.Internal
{
   /// <summary>
   /// 
   /// </summary>
   /// <author>Guenther Grau</author>
   public class DetectionResult
   {
      private const int ADJUST_ROW_NUMBER_SKIP = 2;

      public BarcodeMetadata Metadata { get; private set; }
      public DetectionResultColumn[] DetectionResultColumns { get; set; }
      public BoundingBox Box { get; set; }
      public int ColumnCount { get; private set; }

      public int RowCount
      {
         get { return Metadata.RowCount; }
      }

      public int ErrorCorrectionLevel
      {
         get { return Metadata.ErrorCorrectionLevel; }
      }

      public DetectionResult(BarcodeMetadata metadata, BoundingBox box)
      {
         Metadata = metadata;
         Box = box;
         ColumnCount = metadata.ColumnCount;
         DetectionResultColumns = new DetectionResultColumn[ColumnCount + 2];
      }

      /// <summary>
      /// Returns the DetectionResult Columns.  This does a fair bit of calculation, so call it sparingly.
      /// </summary>
      /// <returns>The detection result columns.</returns>
      public DetectionResultColumn[] getDetectionResultColumns()
      {
         adjustIndicatorColumnRowNumbers(DetectionResultColumns[0]);
         adjustIndicatorColumnRowNumbers(DetectionResultColumns[ColumnCount + 1]);
         int unadjustedCodewordCount = PDF417Common.MAX_CODEWORDS_IN_BARCODE;
         int previousUnadjustedCount;
         do
         {
            previousUnadjustedCount = unadjustedCodewordCount;
            unadjustedCodewordCount = adjustRowNumbers();
         } while (unadjustedCodewordCount > 0 && unadjustedCodewordCount < previousUnadjustedCount);
         return DetectionResultColumns;
      }

      /// <summary>
      /// Adjusts the indicator column row numbers.
      /// </summary>
      /// <param name="detectionResultColumn">Detection result column.</param>
      private void adjustIndicatorColumnRowNumbers(DetectionResultColumn detectionResultColumn)
      {
         if (detectionResultColumn != null)
         {
            ((DetectionResultRowIndicatorColumn) detectionResultColumn)
               .adjustCompleteIndicatorColumnRowNumbers(Metadata);
         }
      }

      /// <summary>
      /// return number of codewords which don't have a valid row number. Note that the count is not accurate as codewords .
      /// will be counted several times. It just serves as an indicator to see when we can stop adjusting row numbers
      /// </summary>
      /// <returns>The row numbers.</returns>
      private int adjustRowNumbers()
      {
         // TODO ensure that no detected codewords with unknown row number are left
         // we should be able to estimate the row height and use it as a hint for the row number
         // we should also fill the rows top to bottom and bottom to top
         int unadjustedCount = adjustRowNumbersByRow();
         if (unadjustedCount == 0)
         {
            return 0;
         }
         for (int barcodeColumn = 1; barcodeColumn < ColumnCount + 1; barcodeColumn++)
         {
            Codeword[] codewords = DetectionResultColumns[barcodeColumn].Codewords;
            for (int codewordsRow = 0; codewordsRow < codewords.Length; codewordsRow++)
            {
               if (codewords[codewordsRow] == null)
               {
                  continue;
               }
               if (!codewords[codewordsRow].HasValidRowNumber)
               {
                  adjustRowNumbers(barcodeColumn, codewordsRow, codewords);
               }
            }
         }
         return unadjustedCount;
      }

      /// <summary>
      /// Adjusts the row numbers by row.
      /// </summary>
      /// <returns>The row numbers by row.</returns>
      private int adjustRowNumbersByRow()
      {
         adjustRowNumbersFromBothRI(); // RI = RowIndicators
         // TODO we should only do full row adjustments if row numbers of left and right row indicator column match.
         // Maybe it's even better to calculated the height (in codeword rows) and divide it by the number of barcode
         // rows. This, together with the LRI and RRI row numbers should allow us to get a good estimate where a row
         // number starts and ends.
         int unadjustedCount = adjustRowNumbersFromLRI();
         return unadjustedCount + adjustRowNumbersFromRRI();
      }

      /// <summary>
      /// Adjusts the row numbers from both Row Indicators
      /// </summary>
      /// <returns> zero </returns>
      private void adjustRowNumbersFromBothRI()
      {
         if (DetectionResultColumns[0] == null || DetectionResultColumns[ColumnCount + 1] == null)
         {
            return;
         }
         Codeword[] LRIcodewords = DetectionResultColumns[0].Codewords;
         Codeword[] RRIcodewords = DetectionResultColumns[ColumnCount + 1].Codewords;
         for (int codewordsRow = 0; codewordsRow < LRIcodewords.Length; codewordsRow++)
         {
            if (LRIcodewords[codewordsRow] != null &&
                RRIcodewords[codewordsRow] != null &&
                LRIcodewords[codewordsRow].RowNumber == RRIcodewords[codewordsRow].RowNumber)
            {
               for (int barcodeColumn = 1; barcodeColumn <= ColumnCount; barcodeColumn++)
               {
                  Codeword codeword = DetectionResultColumns[barcodeColumn].Codewords[codewordsRow];
                  if (codeword == null)
                  {
                     continue;
                  }
                  codeword.RowNumber = LRIcodewords[codewordsRow].RowNumber;
                  if (!codeword.HasValidRowNumber)
                  {
                     // LOG.info("Removing codeword with invalid row number, cw[" + codewordsRow + "][" + barcodeColumn + "]");
                     DetectionResultColumns[barcodeColumn].Codewords[codewordsRow] = null;
                  }
               }
            }
         }
      }

      /// <summary>
      /// Adjusts the row numbers from Right Row Indicator.
      /// </summary>
      /// <returns>The unadjusted row count.</returns>
      private int adjustRowNumbersFromRRI()
      {
         if (DetectionResultColumns[ColumnCount + 1] == null)
         {
            return 0;
         }
         int unadjustedCount = 0;
         Codeword[] codewords = DetectionResultColumns[ColumnCount + 1].Codewords;
         for (int codewordsRow = 0; codewordsRow < codewords.Length; codewordsRow++)
         {
            if (codewords[codewordsRow] == null)
            {
               continue;
            }
            int rowIndicatorRowNumber = codewords[codewordsRow].RowNumber;
            int invalidRowCounts = 0;
            for (int barcodeColumn = ColumnCount + 1; barcodeColumn > 0 && invalidRowCounts < ADJUST_ROW_NUMBER_SKIP; barcodeColumn--)
            {
               Codeword codeword = DetectionResultColumns[barcodeColumn].Codewords[codewordsRow];
               if (codeword != null)
               {
                  invalidRowCounts = adjustRowNumberIfValid(rowIndicatorRowNumber, invalidRowCounts, codeword);
                  if (!codeword.HasValidRowNumber)
                  {
                     unadjustedCount++;
                  }
               }
            }
         }
         return unadjustedCount;
      }

      /// <summary>
      /// Adjusts the row numbers from Left Row Indicator.
      /// </summary>
      /// <returns> Unadjusted row Count.</returns>
      private int adjustRowNumbersFromLRI()
      {
         if (DetectionResultColumns[0] == null)
         {
            return 0;
         }
         int unadjustedCount = 0;
         Codeword[] codewords = DetectionResultColumns[0].Codewords;
         for (int codewordsRow = 0; codewordsRow < codewords.Length; codewordsRow++)
         {
            if (codewords[codewordsRow] == null)
            {
               continue;
            }
            int rowIndicatorRowNumber = codewords[codewordsRow].RowNumber;
            int invalidRowCounts = 0;
            for (int barcodeColumn = 1; barcodeColumn < ColumnCount + 1 && invalidRowCounts < ADJUST_ROW_NUMBER_SKIP; barcodeColumn++)
            {
               Codeword codeword = DetectionResultColumns[barcodeColumn].Codewords[codewordsRow];
               if (codeword != null)
               {
                  invalidRowCounts = adjustRowNumberIfValid(rowIndicatorRowNumber, invalidRowCounts, codeword);
                  if (!codeword.HasValidRowNumber)
                  {
                     unadjustedCount++;
                  }
               }
            }
         }
         return unadjustedCount;
      }

      /// <summary>
      /// Adjusts the row number if valid.
      /// </summary>
      /// <returns>The invalid rows</returns>
      /// <param name="rowIndicatorRowNumber">Row indicator row number.</param>
      /// <param name="invalidRowCounts">Invalid row counts.</param>
      /// <param name="codeword">Codeword.</param>
      private static int adjustRowNumberIfValid(int rowIndicatorRowNumber, int invalidRowCounts, Codeword codeword)
      {

         if (codeword == null)
         {
            return invalidRowCounts;
         }
         if (!codeword.HasValidRowNumber)
         {
            if (codeword.IsValidRowNumber(rowIndicatorRowNumber))
            {
               codeword.RowNumber = rowIndicatorRowNumber;
               invalidRowCounts = 0;
            }
            else
            {
               ++invalidRowCounts;
            }
         }
         return invalidRowCounts;
      }

      /// <summary>
      /// Adjusts the row numbers.
      /// </summary>
      /// <param name="barcodeColumn">Barcode column.</param>
      /// <param name="codewordsRow">Codewords row.</param>
      /// <param name="codewords">Codewords.</param>
      private void adjustRowNumbers(int barcodeColumn, int codewordsRow, Codeword[] codewords)
      {
         Codeword codeword = codewords[codewordsRow];
         Codeword[] previousColumnCodewords = DetectionResultColumns[barcodeColumn - 1].Codewords;
         Codeword[] nextColumnCodewords = previousColumnCodewords;
         if (DetectionResultColumns[barcodeColumn + 1] != null)
         {
            nextColumnCodewords = DetectionResultColumns[barcodeColumn + 1].Codewords;
         }

         Codeword[] otherCodewords = new Codeword[14];

         otherCodewords[2] = previousColumnCodewords[codewordsRow];
         otherCodewords[3] = nextColumnCodewords[codewordsRow];

         if (codewordsRow > 0)
         {
            otherCodewords[0] = codewords[codewordsRow - 1];
            otherCodewords[4] = previousColumnCodewords[codewordsRow - 1];
            otherCodewords[5] = nextColumnCodewords[codewordsRow - 1];
         }
         if (codewordsRow > 1)
         {
            otherCodewords[8] = codewords[codewordsRow - 2];
            otherCodewords[10] = previousColumnCodewords[codewordsRow - 2];
            otherCodewords[11] = nextColumnCodewords[codewordsRow - 2];
         }
         if (codewordsRow < codewords.Length - 1)
         {
            otherCodewords[1] = codewords[codewordsRow + 1];
            otherCodewords[6] = previousColumnCodewords[codewordsRow + 1];
            otherCodewords[7] = nextColumnCodewords[codewordsRow + 1];
         }
         if (codewordsRow < codewords.Length - 2)
         {
            otherCodewords[9] = codewords[codewordsRow + 2];
            otherCodewords[12] = previousColumnCodewords[codewordsRow + 2];
            otherCodewords[13] = nextColumnCodewords[codewordsRow + 2];
         }
         foreach (Codeword otherCodeword in otherCodewords)
         {
            if (adjustRowNumber(codeword, otherCodeword))
            {
               return;
            }
         }
      }

      /// <summary>
      /// Adjusts the row number.
      /// </summary>
      /// <returns><c>true</c>, if row number was adjusted, <c>false</c> otherwise.</returns>
      /// <param name="codeword">Codeword.</param>
      /// <param name="otherCodeword">Other codeword.</param>
      private static bool adjustRowNumber(Codeword codeword, Codeword otherCodeword)
      {
         if (otherCodeword == null)
         {
            return false;
         }
         if (otherCodeword.HasValidRowNumber && otherCodeword.Bucket == codeword.Bucket)
         {
            codeword.RowNumber = otherCodeword.RowNumber;
            return true;
         }
         return false;
      }

      /// <summary>
      /// Returns a <see cref="System.String"/> that represents the current <see cref="ZXing.PDF417.Internal.DetectionResult"/>.
      /// </summary>
      /// <returns>A <see cref="System.String"/> that represents the current <see cref="ZXing.PDF417.Internal.DetectionResult"/>.</returns>
      public override string ToString()
      {
         StringBuilder formatter = new StringBuilder();
         DetectionResultColumn rowIndicatorColumn = DetectionResultColumns[0];
         if (rowIndicatorColumn == null)
         {
            rowIndicatorColumn = DetectionResultColumns[ColumnCount + 1];
         }
         for (int codewordsRow = 0; codewordsRow < rowIndicatorColumn.Codewords.Length; codewordsRow++)
         {
            formatter.AppendFormat(CultureInfo.InvariantCulture, "CW {0,3}:", codewordsRow);
            for (int barcodeColumn = 0; barcodeColumn < ColumnCount + 2; barcodeColumn++)
            {
               if (DetectionResultColumns[barcodeColumn] == null)
               {
                  formatter.Append("    |   ");
                  continue;
               }
               Codeword codeword = DetectionResultColumns[barcodeColumn].Codewords[codewordsRow];
               if (codeword == null)
               {
                  formatter.Append("    |   ");
                  continue;
               }
               formatter.AppendFormat(CultureInfo.InvariantCulture, " {0,3}|{1,3}", codeword.RowNumber, codeword.Value);
            }
            formatter.Append("\n");
         }

         return formatter.ToString();
      }
   }
}