/*
 * Copyright 2013 Artur Mkrtchyan
 *
 * 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.
 *
 *
 * elect: Deutsche Version...
 */

package org.iban4j;

import de.ivu.elect.jee.faces.FacesMessageUtil;
import org.iban4j.bban.BbanStructure;
import org.iban4j.bban.BbanStructureEntry;
import org.iban4j.support.Assert;

import static org.iban4j.Iban.*;

/**
 * ELECT-3129 Iban Bic Validierung lokalisieren
 */
public final class IbanUtilDE {

  private static final int MOD = 97;
  private static final long MAX = 999999999;

  private static final int DE_MIN_IBAN_LENGTH = 15;
  private static final int DE_BANKLEITZAHL_LENGTH = 8;
  private static final String DE_COUNTRY_CODE = "DE";

  private IbanUtilDE() {
  }

  /**
   * Calculates Iban <a href="http://en.wikipedia.org/wiki/ISO_13616#Generating_IBAN_check_digits">Check Digit</a>.
   *
   * @param iban
   *          string value
   * @return check digit as String
   */
  public static String calculateCheckDigit(final String iban) {
    String reformattedIban = removeCheckDigit(iban);
    int modResult = calculateMod(reformattedIban);
    int checkDigitIntValue = 98 - modResult;
    String checkDigit = Integer.toString(checkDigitIntValue);
    return checkDigitIntValue > 9 ? checkDigit : "0" + checkDigit;
  }

  /**
   * Validates iban.
   *
   * @param iban
   *          to be validated.
   *
   * @throws IbanFormatException
   *           if iban is invalid.
   * @throws UnsupportedCountryException
   *           if iban's country is not supported.
   * @throws InvalidCheckDigitException
   *           if iban has invalid check digit.
   */
  public static void validate(final String iban) throws IbanFormatException, InvalidCheckDigitException, UnsupportedCountryException {
    try {
      Assert.notNull(iban, FacesMessageUtil.getResourceText("iban.validation.detail.empty"));

      validateCountryCode(iban);
      validateMinLength(iban);

      BbanStructure structure = getBbanStructure(iban);

      validateBbanLength(iban, structure);
      validateBbanEntries(iban, structure);

      validateCheckDigit(iban);
    } catch (UnsupportedCountryException e) {
      throw e;
    } catch (InvalidCheckDigitException e) {
      throw e;
    } catch (RuntimeException e) {
      // e.printStackTrace();
      throw new IbanFormatException(e.getMessage());
    }
  }

  protected static void validateCheckDigit(final String iban) {
    String checkDigit = getCheckDigit(iban);
    String expectedCheckDigit = calculateCheckDigit(iban);
    if (!checkDigit.equals(expectedCheckDigit)) {

      throw new InvalidCheckDigitException(
          FacesMessageUtil.getResourceText("iban.validation.detail.invalid.check.digit", iban, checkDigit, expectedCheckDigit));
    }
  }

  // Cf. BicUtilDE.validateCountryCode!
  private static void validateCountryCode(final String iban) {
    String countryCode = getCountryCode(iban);

    if (countryCode.trim().length() < COUNTRY_CODE_LENGTH || !countryCode.equals(countryCode.toUpperCase())
        || !Character.isLetter(countryCode.charAt(0)) || !Character.isLetter(countryCode.charAt(1))) {
      throw new IbanFormatException(FacesMessageUtil.getResourceText("iban.validation.detail.country.code.not.upper.case"));
    }

    Assert.notNull(CountryCode.getByCode(countryCode), FacesMessageUtil.getResourceText("iban.validation.detail.country.code.unknown", countryCode));
  }

  private static void validateMinLength(final String iban) {
    if (iban.length() < DE_MIN_IBAN_LENGTH) {
      throw new IbanFormatException(FacesMessageUtil.getResourceText("iban.validation.detail.invalid.min.length", DE_MIN_IBAN_LENGTH));
    }
  }

  private static void validateBbanLength(final String iban, final BbanStructure structure) {
    int expectedBbanLength = structure.getBbanLength();
    String bban = getBban(iban);
    int bbanLength = bban.length();
    if (expectedBbanLength != bbanLength) {
      throw new IbanFormatException(FacesMessageUtil.getResourceText("iban.validation.detail.invalid.bban.length", bban, bbanLength, expectedBbanLength));
    }
  }

  private static void validateBbanEntries(final String iban, final BbanStructure structure) {
    final String bban = getBban(iban);
    int bbanEntryOffset = 0;
    for (BbanStructureEntry entry : structure.getEntries()) {
      final int entryLength = entry.getLength();
      final String entryValue = bban.substring(bbanEntryOffset, bbanEntryOffset + entryLength);

      bbanEntryOffset = bbanEntryOffset + entryLength;

      // validate character type
      validateBbanEntryCharacterType(entry, entryValue);
    }
  }

  private static void validateBbanEntryCharacterType(final BbanStructureEntry entry, final String entryValue) {
    switch (entry.getCharacterType()) {
      case a :
        for (char ch : entryValue.toCharArray()) {
          Assert.isTrue(Character.isUpperCase(ch), FacesMessageUtil.getResourceText("iban.validation.detail.bban.character.not.upper.case", entryValue));
        }
        break;
      case c :
        for (char ch : entryValue.toCharArray()) {
          Assert.isTrue(Character.isLetterOrDigit(ch), FacesMessageUtil.getResourceText("iban.validation.detail.bban.character.not.digit.or.letter", entryValue));
        }
        break;
      case n :
        for (char ch : entryValue.toCharArray()) {
          Assert.isTrue(Character.isDigit(ch), FacesMessageUtil.getResourceText("iban.validation.detail.bban.character.not.digit", entryValue));
        }
        break;
    }
  }

  protected static String calculateCheckDigit(final Iban iban) {
    return calculateCheckDigit(iban.toString());
  }

  /**
   * Returns an iban with default check digit.
   *
   * @param iban
   *          The iban
   * @return The iban without the check digit
   */
  private static String removeCheckDigit(final String iban) {
    return getCountryCode(iban) + DEFAULT_CHECK_DIGIT + getBban(iban);
  }

  /**
   * Calculates <a href="http://en.wikipedia.org/wiki/ISO_13616#Modulo_operation_on_IBAN">Iban Modulo</a>.
   *
   * @param iban
   *          String value
   * @return modulo 97
   */
  private static int calculateMod(final String iban) {
    String reformattedIban = getBban(iban) + getCountryCode(iban) + getCheckDigit(iban);
    long total = 0;
    for (int i = 0; i < reformattedIban.length(); i++) {
      int numericValue = Character.getNumericValue(reformattedIban.charAt(i));
      if (numericValue < 0 || numericValue > 35) {
        throw new IllegalArgumentException(FacesMessageUtil.getResourceText("iban.validation.detail.invalid.character", i, numericValue));
      }
      total = (numericValue > 9 ? total * 100 : total * 10) + numericValue;

      if (total > MAX) {
        total = total % MOD;
      }

    }
    return (int) (total % MOD);
  }

  private static BbanStructure getBbanStructure(final String iban) {
    String countryCode = getCountryCode(iban);
    return BbanStructure.forCountry(CountryCode.getByCode(countryCode));
  }

  private static String getCheckDigit(final String iban) {
    return iban.substring(CHECK_DIGIT_INDEX, CHECK_DIGIT_INDEX + CHECK_DIGIT_LENGTH);
  }

  private static String getCountryCode(final String iban) {
    int endIndex = COUNTRY_CODE_INDEX + COUNTRY_CODE_LENGTH;

    if (iban.length() >= endIndex) {
      return iban.substring(COUNTRY_CODE_INDEX, endIndex);
    }

    return "";
  }

  private static String getBban(final String iban) {
    if (iban.length() >= BBAN_INDEX) {
      return iban.substring(BBAN_INDEX);
    }
    return "";
  }

  public static boolean isCountryDE(String iban) {
    CountryCode countryCode = CountryCode.getByCode(getCountryCode(iban));
    if (countryCode == null) {
      return false;
    }
    return DE_COUNTRY_CODE.equals(countryCode.getAlpha2());
  }

  public static String getBankleitzahl(String iban) {
    if (!isCountryDE(iban) || getBban(iban).length() < DE_BANKLEITZAHL_LENGTH) {
      return "";
    }
    return getBban(iban).substring(0, DE_BANKLEITZAHL_LENGTH);
  }
}
