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

        /// <summary>
        /// metadata which are found
        /// </summary>
        public BarcodeMetadata Metadata { get; private set; }
        /// <summary>
        /// result columns
        /// </summary>
        public DetectionResultColumn[] DetectionResultColumns { get; set; }
        /// <summary>
        /// bounding box of the detected result
        /// </summary>
        public BoundingBox Box { get; internal set; }
        /// <summary>
        /// column count
        /// </summary>
        public int ColumnCount { get; private set; }
        /// <summary>
        /// row count
        /// </summary>
        public int RowCount
        {
            get { return Metadata.RowCount; }
        }
        /// <summary>
        /// error correction level
        /// </summary>
        public int ErrorCorrectionLevel
        {
            get { return Metadata.ErrorCorrectionLevel; }
        }

        /// <summary>
        /// initializing constructor
        /// </summary>
        /// <param name="metadata"></param>
        /// <param name="box"></param>
        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();
        }
    }
}