/*
 * Copyright 2012 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.
 */

#import "ZXDecoderResult.h"
#import "ZXErrors.h"
#import "ZXPDF417DecodedBitStreamParser.h"
#import "ZXPDF417ResultMetadata.h"

enum {
  ALPHA,
  LOWER,
  MIXED,
  PUNCT,
  ALPHA_SHIFT,
  PUNCT_SHIFT
};

int const TEXT_COMPACTION_MODE_LATCH = 900;
int const BYTE_COMPACTION_MODE_LATCH = 901;
int const NUMERIC_COMPACTION_MODE_LATCH = 902;
int const BYTE_COMPACTION_MODE_LATCH_6 = 924;
int const BEGIN_MACRO_PDF417_CONTROL_BLOCK = 928;
int const BEGIN_MACRO_PDF417_OPTIONAL_FIELD = 923;
int const MACRO_PDF417_TERMINATOR = 922;
int const MODE_SHIFT_TO_BYTE_COMPACTION_MODE = 913;
int const MAX_NUMERIC_CODEWORDS = 15;

int const PL = 25;
int const LL = 27;
int const AS = 27;
int const ML = 28;
int const AL = 28;
int const PS = 29;
int const PAL = 29;

char const PUNCT_CHARS[29] = {
  ';', '<', '>', '@', '[', '\\', '}', '_', '`', '~', '!',
  '\r', '\t', ',', ':', '\n', '-', '.', '$', '/', '"', '|', '*',
  '(', ')', '?', '{', '}', '\''};

char const MIXED_CHARS[25] = {
  '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '&',
  '\r', '\t', ',', ':', '#', '-', '.', '$', '/', '+', '%', '*',
  '=', '^'};

int const NUMBER_OF_SEQUENCE_CODEWORDS = 2;

/**
 * Table containing values for the exponent of 900.
 * This is used in the numeric compaction decode algorithm.
 */
static NSArray *EXP900 = nil;

@implementation ZXPDF417DecodedBitStreamParser

+ (void)initialize {
  NSMutableArray *exponents = [NSMutableArray arrayWithCapacity:16];
  [exponents addObject:[NSDecimalNumber one]];
  NSDecimalNumber *nineHundred = [NSDecimalNumber decimalNumberWithString:@"900"];
  [exponents addObject:nineHundred];
  for (int i = 2; i < 16; i++) {
    [exponents addObject:[exponents[i - 1] decimalNumberByMultiplyingBy:nineHundred]];
  }
  EXP900 = [[NSArray alloc] initWithArray:exponents];
}

+ (ZXDecoderResult *)decode:(NSArray *)codewords ecLevel:(NSString *)ecLevel error:(NSError **)error {
  if (!codewords) {
    if (error) *error = NotFoundErrorInstance();
    return nil;
  }
  NSMutableString *result = [NSMutableString stringWithCapacity:codewords.count * 2];
  // Get compaction mode
  int codeIndex = 1;
  int code = [codewords[codeIndex++] intValue];
  ZXPDF417ResultMetadata *resultMetadata = [[ZXPDF417ResultMetadata alloc] init];
  while (codeIndex < [codewords[0] intValue]) {
    switch (code) {
    case TEXT_COMPACTION_MODE_LATCH:
      codeIndex = [self textCompaction:codewords codeIndex:codeIndex result:result];
      break;
    case BYTE_COMPACTION_MODE_LATCH:
      codeIndex = [self byteCompaction:code codewords:codewords codeIndex:codeIndex result:result];
      break;
    case NUMERIC_COMPACTION_MODE_LATCH:
      codeIndex = [self numericCompaction:codewords codeIndex:codeIndex result:result];
      break;
    case MODE_SHIFT_TO_BYTE_COMPACTION_MODE:
      codeIndex = [self byteCompaction:code codewords:codewords codeIndex:codeIndex result:result];
      break;
    case BYTE_COMPACTION_MODE_LATCH_6:
      codeIndex = [self byteCompaction:code codewords:codewords codeIndex:codeIndex result:result];
      break;
    case BEGIN_MACRO_PDF417_CONTROL_BLOCK:
      codeIndex = [self decodeMacroBlock:codewords codeIndex:codeIndex resultMetadata:resultMetadata];
      if (codeIndex < 0) {
        if (error) *error = NotFoundErrorInstance();
        return nil;
      }
      break;
    case BEGIN_MACRO_PDF417_OPTIONAL_FIELD:
    case MACRO_PDF417_TERMINATOR:
      // Should not see these outside a macro block
      if (error) *error = NotFoundErrorInstance();
      return nil;
    default:
      // Default to text compaction. During testing numerous barcodes
      // appeared to be missing the starting mode. In these cases defaulting
      // to text compaction seems to work.
      codeIndex--;
      codeIndex = [self textCompaction:codewords codeIndex:codeIndex result:result];
      break;
    }
    if (codeIndex < [codewords count]) {
      code = [codewords[codeIndex++] intValue];
    } else {
      if (error) *error = NotFoundErrorInstance();
      return nil;
    }
  }
  if ([result length] == 0) {
    if (error) *error = NotFoundErrorInstance();
    return nil;
  }
  ZXDecoderResult *decoderResult = [[ZXDecoderResult alloc] initWithRawBytes:NULL length:0 text:result byteSegments:nil ecLevel:ecLevel];
  decoderResult.other = resultMetadata;
  return decoderResult;
}

+ (int)decodeMacroBlock:(NSArray *)codewords codeIndex:(int)codeIndex resultMetadata:(ZXPDF417ResultMetadata *)resultMetadata {
  if (codeIndex + NUMBER_OF_SEQUENCE_CODEWORDS > [codewords[0] intValue]) {
    // we must have at least two bytes left for the segment index
    return -1;
  }
  int segmentIndexArray[NUMBER_OF_SEQUENCE_CODEWORDS];
  memset(segmentIndexArray, 0, NUMBER_OF_SEQUENCE_CODEWORDS * sizeof(int));
  for (int i = 0; i < NUMBER_OF_SEQUENCE_CODEWORDS; i++, codeIndex++) {
    segmentIndexArray[i] = [codewords[codeIndex] intValue];
  }
  resultMetadata.segmentIndex = [[self decodeBase900toBase10:segmentIndexArray count:NUMBER_OF_SEQUENCE_CODEWORDS] intValue];

  NSMutableString *fileId = [NSMutableString string];
  codeIndex = [self textCompaction:codewords codeIndex:codeIndex result:fileId];
  resultMetadata.fileId = [NSString stringWithString:fileId];

  if ([codewords[codeIndex] intValue] == BEGIN_MACRO_PDF417_OPTIONAL_FIELD) {
    codeIndex++;
    NSMutableArray *additionalOptionCodeWords = [NSMutableArray array];

    BOOL end = NO;
    while ((codeIndex < [codewords[0] intValue]) && !end) {
      int code = [codewords[codeIndex++] intValue];
      if (code < TEXT_COMPACTION_MODE_LATCH) {
        [additionalOptionCodeWords addObject:@(code)];
      } else {
        switch (code) {
          case MACRO_PDF417_TERMINATOR:
            resultMetadata.lastSegment = YES;
            codeIndex++;
            end = YES;
            break;
          default:
            return -1;
        }
      }
    }

    resultMetadata.optionalData = additionalOptionCodeWords;
  } else if ([codewords[codeIndex] intValue] == MACRO_PDF417_TERMINATOR) {
    resultMetadata.lastSegment = YES;
    codeIndex++;
  }

  return codeIndex;
}

/**
 * Text Compaction mode (see 5.4.1.5) permits all printable ASCII characters to be
 * encoded, i.e. values 32 - 126 inclusive in accordance with ISO/IEC 646 (IRV), as
 * well as selected control characters.
 */
+ (int)textCompaction:(NSArray *)codewords codeIndex:(int)codeIndex result:(NSMutableString *)result {
  int count = ([codewords[0] intValue] - codeIndex) << 1;
  // 2 character per codeword
  int textCompactionData[count];
  // Used to hold the byte compaction value if there is a mode shift
  int byteCompactionData[count];

  for (int i = 0; i < count; i++) {
    textCompactionData[0] = 0;
    byteCompactionData[0] = 0;
  }

  int index = 0;
  BOOL end = NO;
  while ((codeIndex < [codewords[0] intValue]) && !end) {
    int code = [codewords[codeIndex++] intValue];
    if (code < TEXT_COMPACTION_MODE_LATCH) {
      textCompactionData[index] = code / 30;
      textCompactionData[index + 1] = code % 30;
      index += 2;
    } else {
      switch (code) {
      case TEXT_COMPACTION_MODE_LATCH:
        // reinitialize text compaction mode to alpha sub mode
        textCompactionData[index++] = TEXT_COMPACTION_MODE_LATCH;
        break;
      case BYTE_COMPACTION_MODE_LATCH:
        codeIndex--;
        end = YES;
        break;
      case NUMERIC_COMPACTION_MODE_LATCH:
        codeIndex--;
        end = YES;
        break;
      case BEGIN_MACRO_PDF417_CONTROL_BLOCK:
        codeIndex--;
        end = YES;
        break;
      case BEGIN_MACRO_PDF417_OPTIONAL_FIELD:
        codeIndex--;
        end = YES;
        break;
      case MACRO_PDF417_TERMINATOR:
        codeIndex--;
        end = YES;
        break;
      case MODE_SHIFT_TO_BYTE_COMPACTION_MODE:
        // The Mode Shift codeword 913 shall cause a temporary
        // switch from Text Compaction mode to Byte Compaction mode.
        // This switch shall be in effect for only the next codeword,
        // after which the mode shall revert to the prevailing sub-mode
        // of the Text Compaction mode. Codeword 913 is only available
        // in Text Compaction mode; its use is described in 5.4.2.4.
        textCompactionData[index] = MODE_SHIFT_TO_BYTE_COMPACTION_MODE;
        code = [codewords[codeIndex++] intValue];
        byteCompactionData[index] = code;
        index++;
        break;
      case BYTE_COMPACTION_MODE_LATCH_6:
        codeIndex--;
        end = YES;
        break;
      }
    }
  }

  [self decodeTextCompaction:textCompactionData byteCompactionData:byteCompactionData length:index result:result];
  return codeIndex;
}


/**
 * The Text Compaction mode includes all the printable ASCII characters
 * (i.e. values from 32 to 126) and three ASCII control characters: HT or tab
 * (ASCII value 9), LF or line feed (ASCII value 10), and CR or carriage
 * return (ASCII value 13). The Text Compaction mode also includes various latch
 * and shift characters which are used exclusively within the mode. The Text
 * Compaction mode encodes up to 2 characters per codeword. The compaction rules
 * for converting data into PDF417 codewords are defined in 5.4.2.2. The sub-mode
 * switches are defined in 5.4.2.3.
 */
+ (void)decodeTextCompaction:(int *)textCompactionData byteCompactionData:(int *)byteCompactionData length:(unsigned int)length result:(NSMutableString *)result {
  // Beginning from an initial state of the Alpha sub-mode
  // The default compaction mode for PDF417 in effect at the start of each symbol shall always be Text
  // Compaction mode Alpha sub-mode (uppercase alphabetic). A latch codeword from another mode to the Text
  // Compaction mode shall always switch to the Text Compaction Alpha sub-mode.
  int subMode = ALPHA;
  int priorToShiftMode = ALPHA;
  int i = 0;
  while (i < length) {
    int subModeCh = textCompactionData[i];
    unichar ch = 0;
    switch (subMode) {
      case ALPHA:
        // Alpha (uppercase alphabetic)
        if (subModeCh < 26) {
        // Upper case Alpha Character
          ch = (unichar)('A' + subModeCh);
        } else {
          if (subModeCh == 26) {
            ch = ' ';
          } else if (subModeCh == LL) {
            subMode = LOWER;
          } else if (subModeCh == ML) {
            subMode = MIXED;
          } else if (subModeCh == PS) {
            // Shift to punctuation
            priorToShiftMode = subMode;
            subMode = PUNCT_SHIFT;
          } else if (subModeCh == MODE_SHIFT_TO_BYTE_COMPACTION_MODE) {
            [result appendFormat:@"%C", (unichar)byteCompactionData[i]];
          } else if (subModeCh == TEXT_COMPACTION_MODE_LATCH) {
            subMode = ALPHA;
          }
        }
        break;

      case LOWER:
        // Lower (lowercase alphabetic)
        if (subModeCh < 26) {
          ch = (unichar)('a' + subModeCh);
        } else {
          if (subModeCh == 26) {
            ch = ' ';
          } else if (subModeCh == AS) {
            // Shift to alpha
            priorToShiftMode = subMode;
            subMode = ALPHA_SHIFT;
          } else if (subModeCh == ML) {
            subMode = MIXED;
          } else if (subModeCh == PS) {
            // Shift to punctuation
            priorToShiftMode = subMode;
            subMode = PUNCT_SHIFT;
          } else if (subModeCh == MODE_SHIFT_TO_BYTE_COMPACTION_MODE) {
            [result appendFormat:@"%C", (unichar)byteCompactionData[i]];
          } else if (subModeCh == TEXT_COMPACTION_MODE_LATCH) {
            subMode = ALPHA;
          }
        }
        break;

      case MIXED:
        // Mixed (numeric and some punctuation)
        if (subModeCh < PL) {
          ch = MIXED_CHARS[subModeCh];
        } else {
          if (subModeCh == PL) {
            subMode = PUNCT;
          } else if (subModeCh == 26) {
            ch = ' ';
          } else if (subModeCh == LL) {
            subMode = LOWER;
          } else if (subModeCh == AL) {
            subMode = ALPHA;
          } else if (subModeCh == PS) {
            // Shift to punctuation
            priorToShiftMode = subMode;
            subMode = PUNCT_SHIFT;
          } else if (subModeCh == MODE_SHIFT_TO_BYTE_COMPACTION_MODE) {
            [result appendFormat:@"%C", (unichar)byteCompactionData[i]];
          } else if (subModeCh == TEXT_COMPACTION_MODE_LATCH) {
            subMode = ALPHA;
          }
        }
        break;

      case PUNCT:
        // Punctuation
        if (subModeCh < PAL) {
          ch = PUNCT_CHARS[subModeCh];
        } else {
          if (subModeCh == PAL) {
            subMode = ALPHA;
          } else if (subModeCh == MODE_SHIFT_TO_BYTE_COMPACTION_MODE) {
            [result appendFormat:@"%C", (unichar)byteCompactionData[i]];
          } else if (TEXT_COMPACTION_MODE_LATCH) {
            subMode = ALPHA;
          }
        }
        break;

      case ALPHA_SHIFT:
        // Restore sub-mode
        subMode = priorToShiftMode;
        if (subModeCh < 26) {
          ch = (unichar)('A' + subModeCh);
        } else {
          if (subModeCh == 26) {
            ch = ' ';
          } else if (subModeCh == TEXT_COMPACTION_MODE_LATCH) {
            subMode = ALPHA;
          }
        }
        break;

      case PUNCT_SHIFT:
        // Restore sub-mode
        subMode = priorToShiftMode;
        if (subModeCh < PAL) {
          ch = PUNCT_CHARS[subModeCh];
        } else {
          if (subModeCh == PAL) {
            subMode = ALPHA;
          } else if (subModeCh == MODE_SHIFT_TO_BYTE_COMPACTION_MODE) {
            // PS before Shift-to-Byte is used as a padding character,
            // see 5.4.2.4 of the specification
            [result appendFormat:@"%C", (unichar)byteCompactionData[i]];
          } else if (subModeCh == TEXT_COMPACTION_MODE_LATCH) {
            subMode = ALPHA;
          }
        }
        break;
    }
    if (ch != 0) {
      // Append decoded character to result
      [result appendFormat:@"%C", ch];
    }
    i++;
  }
}


/**
 * Byte Compaction mode (see 5.4.3) permits all 256 possible 8-bit byte values to be encoded.
 * This includes all ASCII characters value 0 to 127 inclusive and provides for international
 * character set support.
 */
+ (int)byteCompaction:(int)mode codewords:(NSArray *)codewords codeIndex:(int)codeIndex result:(NSMutableString *)result {
  if (mode == BYTE_COMPACTION_MODE_LATCH) {
    // Total number of Byte Compaction characters to be encoded
    // is not a multiple of 6
    int count = 0;
    long long value = 0;
    char decodedData[6] = {0, 0, 0, 0, 0, 0};
    int byteCompactedCodewords[6] = {0, 0, 0, 0, 0, 0};
    BOOL end = NO;
    int nextCode = [codewords[codeIndex++] intValue];
    while ((codeIndex < [codewords[0] intValue]) && !end) {
      byteCompactedCodewords[count++] = nextCode;
      // Base 900
      value = 900 * value + nextCode;
      nextCode = [codewords[codeIndex++] intValue];
      // perhaps it should be ok to check only nextCode >= TEXT_COMPACTION_MODE_LATCH
      if (nextCode == TEXT_COMPACTION_MODE_LATCH ||
          nextCode == BYTE_COMPACTION_MODE_LATCH ||
          nextCode == NUMERIC_COMPACTION_MODE_LATCH ||
          nextCode == BYTE_COMPACTION_MODE_LATCH_6 ||
          nextCode == BEGIN_MACRO_PDF417_CONTROL_BLOCK ||
          nextCode == BEGIN_MACRO_PDF417_OPTIONAL_FIELD ||
          nextCode == MACRO_PDF417_TERMINATOR) {
        codeIndex--;
        end = YES;
      } else {
        if ((count % 5 == 0) && (count > 0)) {
          // Decode every 5 codewords
          // Convert to Base 256
          for (int j = 0; j < 6; ++j) {
            decodedData[5 - j] = (char) (value % 256);
            value >>= 8;
          }
          [result appendString:[[NSString alloc] initWithBytes:decodedData length:6 encoding:NSISOLatin1StringEncoding]];
          count = 0;
        }
      }
    }

    // if the end of all codewords is reached the last codeword needs to be added
    if (codeIndex == [codewords[0] intValue] && nextCode < TEXT_COMPACTION_MODE_LATCH) {
      byteCompactedCodewords[count++] = nextCode;
    }

    // If Byte Compaction mode is invoked with codeword 901,
    // the last group of codewords is interpreted directly
    // as one byte per codeword, without compaction.
    for (int i = 0; i < count; i++) {
      [result appendFormat:@"%C", (unichar)byteCompactedCodewords[i]];
    }
  } else if (mode == BYTE_COMPACTION_MODE_LATCH_6) {
    // Total number of Byte Compaction characters to be encoded
    // is an integer multiple of 6
    int count = 0;
    long long value = 0;
    BOOL end = NO;
    while (codeIndex < [codewords[0] intValue] && !end) {
      int code = [codewords[codeIndex++] intValue];
      if (code < TEXT_COMPACTION_MODE_LATCH) {
        count++;
        // Base 900
        value = 900 * value + code;
      } else {
        if (code == TEXT_COMPACTION_MODE_LATCH ||
            code == BYTE_COMPACTION_MODE_LATCH ||
            code == NUMERIC_COMPACTION_MODE_LATCH ||
            code == BYTE_COMPACTION_MODE_LATCH_6 ||
            code == BEGIN_MACRO_PDF417_CONTROL_BLOCK ||
            code == BEGIN_MACRO_PDF417_OPTIONAL_FIELD ||
            code == MACRO_PDF417_TERMINATOR) {
          codeIndex--;
          end = YES;
        }
      }
      if ((count % 5 == 0) && (count > 0)) {
        // Decode every 5 codewords
        // Convert to Base 256
        unichar decodedData[6];
        for (int j = 0; j < 6; ++j) {
          decodedData[5 - j] = (unichar)(value & 0xFF);
          value >>= 8;
        }
        [result appendString:[NSString stringWithCharacters:decodedData length:6]];
        count = 0;
      }
    }
  }
  return codeIndex;
}

/**
 * Numeric Compaction mode (see 5.4.4) permits efficient encoding of numeric data strings.
 */
+ (int)numericCompaction:(NSArray *)codewords codeIndex:(int)codeIndex result:(NSMutableString *)result {
  int count = 0;
  BOOL end = NO;

  int numericCodewords[MAX_NUMERIC_CODEWORDS];
  memset(numericCodewords, 0, MAX_NUMERIC_CODEWORDS * sizeof(int));

  while (codeIndex < [codewords[0] intValue] && !end) {
    int code = [codewords[codeIndex++] intValue];
    if (codeIndex == [codewords[0] intValue]) {
      end = YES;
    }
    if (code < TEXT_COMPACTION_MODE_LATCH) {
      numericCodewords[count] = code;
      count++;
    } else {
      if (code == TEXT_COMPACTION_MODE_LATCH ||
          code == BYTE_COMPACTION_MODE_LATCH ||
          code == BYTE_COMPACTION_MODE_LATCH_6 ||
          code == BEGIN_MACRO_PDF417_CONTROL_BLOCK ||
          code == BEGIN_MACRO_PDF417_OPTIONAL_FIELD ||
          code == MACRO_PDF417_TERMINATOR) {
        codeIndex--;
        end = YES;
      }
    }
    if (count % MAX_NUMERIC_CODEWORDS == 0 || code == NUMERIC_COMPACTION_MODE_LATCH || end) {
      NSString *s = [self decodeBase900toBase10:numericCodewords count:count];
      if (s == nil) {
        return INT_MAX;
      }
      [result appendString:s];
      count = 0;
    }
  }
  return codeIndex;
}

/**
 * Convert a list of Numeric Compacted codewords from Base 900 to Base 10.
 */
/*
   EXAMPLE
   Encode the fifteen digit numeric string 000213298174000
   Prefix the numeric string with a 1 and set the initial value of
   t = 1 000 213 298 174 000
   Calculate codeword 0
   d0 = 1 000 213 298 174 000 mod 900 = 200
   
   t = 1 000 213 298 174 000 div 900 = 1 111 348 109 082
   Calculate codeword 1
   d1 = 1 111 348 109 082 mod 900 = 282
   
   t = 1 111 348 109 082 div 900 = 1 234 831 232
   Calculate codeword 2
   d2 = 1 234 831 232 mod 900 = 632
   
   t = 1 234 831 232 div 900 = 1 372 034
   Calculate codeword 3
   d3 = 1 372 034 mod 900 = 434
   
   t = 1 372 034 div 900 = 1 524
   Calculate codeword 4u
   d4 = 1 524 mod 900 = 624
   
   t = 1 524 div 900 = 1
   Calculate codeword 5
   d5 = 1 mod 900 = 1
   t = 1 div 900 = 0
   Codeword sequence is: 1, 624, 434, 632, 282, 200
   
   Decode the above codewords involves
   1 x 900 power of 5 + 624 x 900 power of 4 + 434 x 900 power of 3 +
   632 x 900 power of 2 + 282 x 900 power of 1 + 200 x 900 power of 0 = 1000213298174000
   
   Remove leading 1 =>  Result is 000213298174000
 */
+ (NSString *)decodeBase900toBase10:(int[])codewords count:(int)count {
  NSDecimalNumber *result = [NSDecimalNumber zero];
  for (int i = 0; i < count; i++) {
    result = [result decimalNumberByAdding:[EXP900[count - i - 1] decimalNumberByMultiplyingBy:[NSDecimalNumber decimalNumberWithDecimal:[@(codewords[i]) decimalValue]]]];
  }
  NSString *resultString = [result stringValue];
  if (![resultString hasPrefix:@"1"]) {
    return nil;
  }
  return [resultString substringFromIndex:1];
}

@end
