/*
 * Copyright 2010 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 System.Text;
using ZXing.Common;

namespace ZXing.OneD
{
    /// <summary>
    ///   <p>Decodes Code 93 barcodes.</p>
    /// 	<author>Sean Owen</author>
    /// <see cref="Code39Reader" />
    /// </summary>
    public sealed class Code93Reader : OneDReader
    {
        // Note that 'abcd' are dummy characters in place of control characters.
        internal const String ALPHABET_STRING = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ-. $/+%abcd*";
        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.
        /// </summary>
        internal static readonly int[] CHARACTER_ENCODINGS = {
                                                    0x114, 0x148, 0x144, 0x142, 0x128, 0x124, 0x122, 0x150, 0x112, 0x10A, // 0-9
                                                    0x1A8, 0x1A4, 0x1A2, 0x194, 0x192, 0x18A, 0x168, 0x164, 0x162, 0x134, // A-J
                                                    0x11A, 0x158, 0x14C, 0x146, 0x12C, 0x116, 0x1B4, 0x1B2, 0x1AC, 0x1A6, // K-T
                                                    0x196, 0x19A, 0x16C, 0x166, 0x136, 0x13A, // U-Z
                                                    0x12E, 0x1D4, 0x1D2, 0x1CA, 0x16E, 0x176, 0x1AE, // - - %
                                                    0x126, 0x1DA, 0x1D6, 0x132, 0x15E, // Control chars? $-*
                                                 };
        internal static readonly int ASTERISK_ENCODING = CHARACTER_ENCODINGS[47];

        private readonly StringBuilder decodeRowResult;
        private readonly int[] counters;

        /// <summary>
        /// Initializes a new instance of the <see cref="Code93Reader"/> class.
        /// </summary>
        public Code93Reader()
        {
            decodeRowResult = new StringBuilder(20);
            counters = new int[6];
        }

        /// <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 = findAsteriskPattern(row);
            if (start == null)
                return null;

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

            char decodedChar;
            int lastStart;
            do
            {
                if (!recordPattern(row, nextStart, counters))
                    return null;

                int pattern = toPattern(counters);
                if (pattern < 0)
                {
                    return null;
                }
                if (!patternToChar(pattern, out decodedChar))
                    return null;
                decodeRowResult.Append(decodedChar);
                lastStart = nextStart;
                foreach (int counter in counters)
                {
                    nextStart += counter;
                }
                // Read off white space
                nextStart = row.getNextSet(nextStart);
            } while (decodedChar != '*');
            decodeRowResult.Remove(decodeRowResult.Length - 1, 1); // remove asterisk

            int lastPatternSize = 0;
            foreach (int counter in counters)
            {
                lastPatternSize += counter;
            }

            // Should be at least one more black module
            if (nextStart == end || !row[nextStart])
            {
                return null;
            }

            if (decodeRowResult.Length < 2)
            {
                // false positive -- need at least 2 checksum digits
                return null;
            }

            if (!checkChecksums(decodeRowResult))
                return null;
            // Remove checksum digits
            decodeRowResult.Length = decodeRowResult.Length - 2;

            String resultString = decodeExtended(decodeRowResult);
            if (resultString == null)
                return null;

            float left = (start[1] + start[0]) / 2.0f;
            float right = lastStart + lastPatternSize / 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,
               null,
               new[]
                  {
                  new ResultPoint(left, rowNumber),
                  new ResultPoint(right, rowNumber)
                  },
               BarcodeFormat.CODE_93);
        }

        private int[] findAsteriskPattern(BitArray row)
        {
            int width = row.Size;
            int rowOffset = row.getNextSet(0);

            for (var index = 0; index < counters.Length; index++)
                counters[index] = 0;
            int counterPosition = 0;
            int patternStart = rowOffset;
            bool isWhite = false;
            int patternLength = counters.Length;

            for (int i = rowOffset; i < width; i++)
            {
                if (row[i] != isWhite)
                {
                    counters[counterPosition]++;
                }
                else
                {
                    if (counterPosition == patternLength - 1)
                    {
                        if (toPattern(counters) == ASTERISK_ENCODING)
                        {
                            return new int[] { patternStart, i };
                        }
                        patternStart += counters[0] + counters[1];
                        Array.Copy(counters, 2, counters, 0, counterPosition - 1);
                        counters[counterPosition - 1] = 0;
                        counters[counterPosition] = 0;
                        counterPosition--;
                    }
                    else
                    {
                        counterPosition++;
                    }
                    counters[counterPosition] = 1;
                    isWhite = !isWhite;
                }
            }
            return null;
        }

        private static int toPattern(int[] counters)
        {
            int max = counters.Length;
            int sum = 0;
            foreach (var counter in counters)
            {
                sum += counter;
            }
            int pattern = 0;
            for (int i = 0; i < max; i++)
            {
                int scaledShifted = (counters[i] << INTEGER_MATH_SHIFT) * 9 / sum;
                int scaledUnshifted = scaledShifted >> INTEGER_MATH_SHIFT;
                if ((scaledShifted & 0xFF) > 0x7F)
                {
                    scaledUnshifted++;
                }
                if (scaledUnshifted < 1 || scaledUnshifted > 4)
                {
                    return -1;
                }
                if ((i & 0x01) == 0)
                {
                    for (int j = 0; j < scaledUnshifted; j++)
                    {
                        pattern = (pattern << 1) | 0x01;
                    }
                }
                else
                {
                    pattern <<= scaledUnshifted;
                }
            }
            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 String decodeExtended(StringBuilder encoded)
        {
            int length = encoded.Length;
            StringBuilder decoded = new StringBuilder(length);
            for (int i = 0; i < length; i++)
            {
                char c = encoded[i];
                if (c >= 'a' && c <= 'd')
                {
                    if (i >= length - 1)
                    {
                        return null;
                    }
                    char next = encoded[i + 1];
                    char decodedChar = '\0';
                    switch (c)
                    {
                        case 'd':
                            // +A to +Z map to a to z
                            if (next >= 'A' && next <= 'Z')
                            {
                                decodedChar = (char)(next + 32);
                            }
                            else
                            {
                                return null;
                            }
                            break;
                        case 'a':
                            // $A to $Z map to control codes SH to SB
                            if (next >= 'A' && next <= 'Z')
                            {
                                decodedChar = (char)(next - 64);
                            }
                            else
                            {
                                return null;
                            }
                            break;
                        case 'b':
                            if (next >= 'A' && next <= 'E')
                            {
                                // %A to %E map to control codes ESC to USep
                                decodedChar = (char)(next - 38);
                            }
                            else if (next >= 'F' && next <= 'J')
                            {
                                // %F to %J map to ; < = > ?
                                decodedChar = (char)(next - 11);
                            }
                            else if (next >= 'K' && next <= 'O')
                            {
                                // %K to %O map to [ \ ] ^ _
                                decodedChar = (char)(next + 16);
                            }
                            else if (next >= 'P' && next <= 'T')
                            {
                                // %P to %T map to { | } ~ DEL
                                decodedChar = (char)(next + 43);
                            }
                            else if (next == 'U')
                            {
                                // %U map to NUL
                                decodedChar = '\0';
                            }
                            else if (next == 'V')
                            {
                                // %V map to @
                                decodedChar = '@';
                            }
                            else if (next == 'W')
                            {
                                // %W map to `
                                decodedChar = '`';
                            }
                            else if (next >= 'X' && next <= 'Z')
                            {
                                // %X to %Z all map to DEL (127)
                                decodedChar = (char)127;
                            }
                            else
                            {
                                return null;
                            }
                            break;
                        case 'c':
                            // /A to /O map to ! to , and /Z maps to :
                            if (next >= 'A' && next <= 'O')
                            {
                                decodedChar = (char)(next - 32);
                            }
                            else if (next == 'Z')
                            {
                                decodedChar = ':';
                            }
                            else
                            {
                                return null;
                            }
                            break;
                    }
                    decoded.Append(decodedChar);
                    // bump up i again since we read two characters
                    i++;
                }
                else
                {
                    decoded.Append(c);
                }
            }
            return decoded.ToString();
        }

        private static bool checkChecksums(StringBuilder result)
        {
            int length = result.Length;
            if (!checkOneChecksum(result, length - 2, 20))
                return false;
            if (!checkOneChecksum(result, length - 1, 15))
                return false;
            return true;
        }

        private static bool checkOneChecksum(StringBuilder result, int checkPosition, int weightMax)
        {
            int weight = 1;
            int total = 0;
            for (int i = checkPosition - 1; i >= 0; i--)
            {
                total += weight * ALPHABET_STRING.IndexOf(result[i]);
                if (++weight > weightMax)
                {
                    weight = 1;
                }
            }
            if (result[checkPosition] != ALPHABET[total % 47])
            {
                return false;
            }
            return true;
        }
    }
}