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

package com.google.zxing.oned.rss;

import com.google.zxing.BarcodeFormat;
import com.google.zxing.DecodeHintType;
import com.google.zxing.NotFoundException;
import com.google.zxing.Result;
import com.google.zxing.ResultMetadataType;
import com.google.zxing.ResultPoint;
import com.google.zxing.ResultPointCallback;
import com.google.zxing.common.BitArray;
import com.google.zxing.common.detector.MathUtils;

import java.util.Arrays;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;

/**
 * Decodes RSS-14, including truncated and stacked variants. See ISO/IEC 24724:2006.
 */
public final class RSS14Reader extends AbstractRSSReader {

  private static final int[] OUTSIDE_EVEN_TOTAL_SUBSET = {1,10,34,70,126};
  private static final int[] INSIDE_ODD_TOTAL_SUBSET = {4,20,48,81};
  private static final int[] OUTSIDE_GSUM = {0,161,961,2015,2715};
  private static final int[] INSIDE_GSUM = {0,336,1036,1516};
  private static final int[] OUTSIDE_ODD_WIDEST = {8,6,4,3,1};
  private static final int[] INSIDE_ODD_WIDEST = {2,4,6,8};

  private static final int[][] FINDER_PATTERNS = {
      {3,8,2,1},
      {3,5,5,1},
      {3,3,7,1},
      {3,1,9,1},
      {2,7,4,1},
      {2,5,6,1},
      {2,3,8,1},
      {1,5,7,1},
      {1,3,9,1},
  };

  private final List<Pair> possibleLeftPairs;
  private final List<Pair> possibleRightPairs;

  public RSS14Reader() {
    possibleLeftPairs = new ArrayList<>();
    possibleRightPairs = new ArrayList<>();
  }

  @Override
  public Result decodeRow(int rowNumber,
                          BitArray row,
                          Map<DecodeHintType,?> hints) throws NotFoundException {
    Pair leftPair = decodePair(row, false, rowNumber, hints);
    addOrTally(possibleLeftPairs, leftPair);
    row.reverse();
    Pair rightPair = decodePair(row, true, rowNumber, hints);
    addOrTally(possibleRightPairs, rightPair);
    row.reverse();
    for (Pair left : possibleLeftPairs) {
      if (left.getCount() > 1) {
        for (Pair right : possibleRightPairs) {
          if (right.getCount() > 1 && checkChecksum(left, right)) {
            return constructResult(left, right);
          }
        }
      }
    }
    throw NotFoundException.getNotFoundInstance();
  }

  private static void addOrTally(Collection<Pair> possiblePairs, Pair pair) {
    if (pair == null) {
      return;
    }
    boolean found = false;
    for (Pair other : possiblePairs) {
      if (other.getValue() == pair.getValue()) {
        other.incrementCount();
        found = true;
        break;
      }
    }
    if (!found) {
      possiblePairs.add(pair);
    }
  }

  @Override
  public void reset() {
    possibleLeftPairs.clear();
    possibleRightPairs.clear();
  }

  private static Result constructResult(Pair leftPair, Pair rightPair) {
    long symbolValue = 4537077L * leftPair.getValue() + rightPair.getValue();
    String text = String.valueOf(symbolValue);

    StringBuilder buffer = new StringBuilder(14);
    for (int i = 13 - text.length(); i > 0; i--) {
      buffer.append('0');
    }
    buffer.append(text);

    int checkDigit = 0;
    for (int i = 0; i < 13; i++) {
      int digit = buffer.charAt(i) - '0';
      checkDigit += (i & 0x01) == 0 ? 3 * digit : digit;
    }
    checkDigit = 10 - (checkDigit % 10);
    if (checkDigit == 10) {
      checkDigit = 0;
    }
    buffer.append(checkDigit);

    ResultPoint[] leftPoints = leftPair.getFinderPattern().getResultPoints();
    ResultPoint[] rightPoints = rightPair.getFinderPattern().getResultPoints();
    Result result = new Result(
        buffer.toString(),
        null,
        new ResultPoint[] { leftPoints[0], leftPoints[1], rightPoints[0], rightPoints[1], },
        BarcodeFormat.RSS_14);
    result.putMetadata(ResultMetadataType.SYMBOLOGY_IDENTIFIER, "]e0");
    return result;
  }

  private static boolean checkChecksum(Pair leftPair, Pair rightPair) {
    int checkValue = (leftPair.getChecksumPortion() + 16 * rightPair.getChecksumPortion()) % 79;
    int targetCheckValue =
        9 * leftPair.getFinderPattern().getValue() + rightPair.getFinderPattern().getValue();
    if (targetCheckValue > 72) {
      targetCheckValue--;
    }
    if (targetCheckValue > 8) {
      targetCheckValue--;
    }
    return checkValue == targetCheckValue;
  }

  private Pair decodePair(BitArray row, boolean right, int rowNumber, Map<DecodeHintType,?> hints) {
    try {
      int[] startEnd = findFinderPattern(row, right);
      FinderPattern pattern = parseFoundFinderPattern(row, rowNumber, right, startEnd);

      ResultPointCallback resultPointCallback = hints == null ? null :
          (ResultPointCallback) hints.get(DecodeHintType.NEED_RESULT_POINT_CALLBACK);

      if (resultPointCallback != null) {
        startEnd = pattern.getStartEnd();
        float center = (startEnd[0] + startEnd[1] - 1) / 2.0f;
        if (right) {
          // row is actually reversed
          center = row.getSize() - 1 - center;
        }
        resultPointCallback.foundPossibleResultPoint(new ResultPoint(center, rowNumber));
      }

      DataCharacter outside = decodeDataCharacter(row, pattern, true);
      DataCharacter inside = decodeDataCharacter(row, pattern, false);
      return new Pair(1597 * outside.getValue() + inside.getValue(),
                      outside.getChecksumPortion() + 4 * inside.getChecksumPortion(),
                      pattern);
    } catch (NotFoundException ignored) {
      return null;
    }
  }

  private DataCharacter decodeDataCharacter(BitArray row, FinderPattern pattern, boolean outsideChar)
      throws NotFoundException {

    int[] counters = getDataCharacterCounters();
    Arrays.fill(counters, 0);

    if (outsideChar) {
      recordPatternInReverse(row, pattern.getStartEnd()[0], counters);
    } else {
      recordPattern(row, pattern.getStartEnd()[1], counters);
      // reverse it
      for (int i = 0, j = counters.length - 1; i < j; i++, j--) {
        int temp = counters[i];
        counters[i] = counters[j];
        counters[j] = temp;
      }
    }

    int numModules = outsideChar ? 16 : 15;
    float elementWidth = MathUtils.sum(counters) / (float) numModules;

    int[] oddCounts = this.getOddCounts();
    int[] evenCounts = this.getEvenCounts();
    float[] oddRoundingErrors = this.getOddRoundingErrors();
    float[] evenRoundingErrors = this.getEvenRoundingErrors();

    for (int i = 0; i < counters.length; i++) {
      float value = counters[i] / elementWidth;
      int count = (int) (value + 0.5f); // Round
      if (count < 1) {
        count = 1;
      } else if (count > 8) {
        count = 8;
      }
      int offset = i / 2;
      if ((i & 0x01) == 0) {
        oddCounts[offset] = count;
        oddRoundingErrors[offset] = value - count;
      } else {
        evenCounts[offset] = count;
        evenRoundingErrors[offset] = value - count;
      }
    }

    adjustOddEvenCounts(outsideChar, numModules);

    int oddSum = 0;
    int oddChecksumPortion = 0;
    for (int i = oddCounts.length - 1; i >= 0; i--) {
      oddChecksumPortion *= 9;
      oddChecksumPortion += oddCounts[i];
      oddSum += oddCounts[i];
    }
    int evenChecksumPortion = 0;
    int evenSum = 0;
    for (int i = evenCounts.length - 1; i >= 0; i--) {
      evenChecksumPortion *= 9;
      evenChecksumPortion += evenCounts[i];
      evenSum += evenCounts[i];
    }
    int checksumPortion = oddChecksumPortion + 3 * evenChecksumPortion;

    if (outsideChar) {
      if ((oddSum & 0x01) != 0 || oddSum > 12 || oddSum < 4) {
        throw NotFoundException.getNotFoundInstance();
      }
      int group = (12 - oddSum) / 2;
      int oddWidest = OUTSIDE_ODD_WIDEST[group];
      int evenWidest = 9 - oddWidest;
      int vOdd = RSSUtils.getRSSvalue(oddCounts, oddWidest, false);
      int vEven = RSSUtils.getRSSvalue(evenCounts, evenWidest, true);
      int tEven = OUTSIDE_EVEN_TOTAL_SUBSET[group];
      int gSum = OUTSIDE_GSUM[group];
      return new DataCharacter(vOdd * tEven + vEven + gSum, checksumPortion);
    } else {
      if ((evenSum & 0x01) != 0 || evenSum > 10 || evenSum < 4) {
        throw NotFoundException.getNotFoundInstance();
      }
      int group = (10 - evenSum) / 2;
      int oddWidest = INSIDE_ODD_WIDEST[group];
      int evenWidest = 9 - oddWidest;
      int vOdd = RSSUtils.getRSSvalue(oddCounts, oddWidest, true);
      int vEven = RSSUtils.getRSSvalue(evenCounts, evenWidest, false);
      int tOdd = INSIDE_ODD_TOTAL_SUBSET[group];
      int gSum = INSIDE_GSUM[group];
      return new DataCharacter(vEven * tOdd + vOdd + gSum, checksumPortion);
    }

  }

  private int[] findFinderPattern(BitArray row, boolean rightFinderPattern)
      throws NotFoundException {

    int[] counters = getDecodeFinderCounters();
    counters[0] = 0;
    counters[1] = 0;
    counters[2] = 0;
    counters[3] = 0;

    int width = row.getSize();
    boolean isWhite = false;
    int rowOffset = 0;
    while (rowOffset < width) {
      isWhite = !row.get(rowOffset);
      if (rightFinderPattern == isWhite) {
        // Will encounter white first when searching for right finder pattern
        break;
      }
      rowOffset++;
    }

    int counterPosition = 0;
    int patternStart = rowOffset;
    for (int x = rowOffset; x < width; x++) {
      if (row.get(x) != isWhite) {
        counters[counterPosition]++;
      } else {
        if (counterPosition == 3) {
          if (isFinderPattern(counters)) {
            return new int[]{patternStart, x};
          }
          patternStart += counters[0] + counters[1];
          counters[0] = counters[2];
          counters[1] = counters[3];
          counters[2] = 0;
          counters[3] = 0;
          counterPosition--;
        } else {
          counterPosition++;
        }
        counters[counterPosition] = 1;
        isWhite = !isWhite;
      }
    }
    throw NotFoundException.getNotFoundInstance();

  }

  private FinderPattern parseFoundFinderPattern(BitArray row, int rowNumber, boolean right, int[] startEnd)
      throws NotFoundException {
    // Actually we found elements 2-5
    boolean firstIsBlack = row.get(startEnd[0]);
    int firstElementStart = startEnd[0] - 1;
    // Locate element 1
    while (firstElementStart >= 0 && firstIsBlack != row.get(firstElementStart)) {
      firstElementStart--;
    }
    firstElementStart++;
    int firstCounter = startEnd[0] - firstElementStart;
    // Make 'counters' hold 1-4
    int[] counters = getDecodeFinderCounters();
    System.arraycopy(counters, 0, counters, 1, counters.length - 1);
    counters[0] = firstCounter;
    int value = parseFinderValue(counters, FINDER_PATTERNS);
    int start = firstElementStart;
    int end = startEnd[1];
    if (right) {
      // row is actually reversed
      start = row.getSize() - 1 - start;
      end = row.getSize() - 1 - end;
    }
    return new FinderPattern(value, new int[] {firstElementStart, startEnd[1]}, start, end, rowNumber);
  }

  private void adjustOddEvenCounts(boolean outsideChar, int numModules) throws NotFoundException {

    int oddSum = MathUtils.sum(getOddCounts());
    int evenSum = MathUtils.sum(getEvenCounts());

    boolean incrementOdd = false;
    boolean decrementOdd = false;
    boolean incrementEven = false;
    boolean decrementEven = false;

    if (outsideChar) {
      if (oddSum > 12) {
        decrementOdd = true;
      } else if (oddSum < 4) {
        incrementOdd = true;
      }
      if (evenSum > 12) {
        decrementEven = true;
      } else if (evenSum < 4) {
        incrementEven = true;
      }
    } else {
      if (oddSum > 11) {
        decrementOdd = true;
      } else if (oddSum < 5) {
        incrementOdd = true;
      }
      if (evenSum > 10) {
        decrementEven = true;
      } else if (evenSum < 4) {
        incrementEven = true;
      }
    }

    int mismatch = oddSum + evenSum - numModules;
    boolean oddParityBad = (oddSum & 0x01) == (outsideChar ? 1 : 0);
    boolean evenParityBad = (evenSum & 0x01) == 1;
    /*if (mismatch == 2) {
      if (!(oddParityBad && evenParityBad)) {
        throw ReaderException.getInstance();
      }
      decrementOdd = true;
      decrementEven = true;
    } else if (mismatch == -2) {
      if (!(oddParityBad && evenParityBad)) {
        throw ReaderException.getInstance();
      }
      incrementOdd = true;
      incrementEven = true;
    } else */
    switch (mismatch) {
      case 1:
        if (oddParityBad) {
          if (evenParityBad) {
            throw NotFoundException.getNotFoundInstance();
          }
          decrementOdd = true;
        } else {
          if (!evenParityBad) {
            throw NotFoundException.getNotFoundInstance();
          }
          decrementEven = true;
        }
        break;
      case -1:
        if (oddParityBad) {
          if (evenParityBad) {
            throw NotFoundException.getNotFoundInstance();
          }
          incrementOdd = true;
        } else {
          if (!evenParityBad) {
            throw NotFoundException.getNotFoundInstance();
          }
          incrementEven = true;
        }
        break;
      case 0:
        if (oddParityBad) {
          if (!evenParityBad) {
            throw NotFoundException.getNotFoundInstance();
          }
          // Both bad
          if (oddSum < evenSum) {
            incrementOdd = true;
            decrementEven = true;
          } else {
            decrementOdd = true;
            incrementEven = true;
          }
        } else {
          if (evenParityBad) {
            throw NotFoundException.getNotFoundInstance();
          }
          // Nothing to do!
        }
        break;
      default:
        throw NotFoundException.getNotFoundInstance();
    }

    if (incrementOdd) {
      if (decrementOdd) {
        throw NotFoundException.getNotFoundInstance();
      }
      increment(getOddCounts(), getOddRoundingErrors());
    }
    if (decrementOdd) {
      decrement(getOddCounts(), getOddRoundingErrors());
    }
    if (incrementEven) {
      if (decrementEven) {
        throw NotFoundException.getNotFoundInstance();
      }
      increment(getEvenCounts(), getOddRoundingErrors());
    }
    if (decrementEven) {
      decrement(getEvenCounts(), getEvenRoundingErrors());
    }

  }

}
