/*
 * Copyright (c) 2001, 2023, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package build.tools.generatecurrencydata;

import java.io.IOException;
import java.io.FileNotFoundException;
import java.io.DataOutputStream;
import java.io.FileOutputStream;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.Locale;
import java.util.Objects;
import java.util.Properties;
import java.util.TimeZone;

/**
 * Reads currency data in properties format from the file specified in the
 * command line and generates a binary data file as specified in the command line.
 *
 * Output of this tool is a binary file that contains the data in
 * the following order:
 *
 *     - magic number (int): always 0x43757244 ('CurD')
 *     - formatVersion (int)
 *     - dataVersion (int)
 *     - mainTable (int[26*26])
 *     - specialCaseCount (int)
 *     - specialCaseCutOverTimes (long[specialCaseCount])
 *     - specialCaseOldCurrencies (String[specialCaseCount])
 *     - specialCaseNewCurrencies (String[specialCaseCount])
 *     - specialCaseOldCurrenciesDefaultFractionDigits (int[specialCaseCount])
 *     - specialCaseNewCurrenciesDefaultFractionDigits (int[specialCaseCount])
 *     - specialCaseOldCurrenciesNumericCode (int[specialCaseCount])
 *     - specialCaseNewCurrenciesNumericCode (int[specialCaseCount])
 *     - otherCurrenciesCount (int)
 *     - otherCurrencies (String)
 *     - otherCurrenciesDefaultFractionDigits (int[otherCurrenciesCount])
 *     - otherCurrenciesNumericCode (int[otherCurrenciesCount])
 *
 * See CurrencyData.properties for the input format description and
 * Currency.java for the format descriptions of the generated tables.
 */
public class GenerateCurrencyData {

    private static DataOutputStream out;

    // input data: currency data obtained from properties on input stream
    private static Properties currencyData;
    private static String formatVersion;
    private static String dataVersion;
    private static String validCurrencyCodes;

    // handy constants - must match definitions in java.util.Currency
    // magic number
    private static final int MAGIC_NUMBER = 0x43757244;
    // number of characters from A to Z
    private static final int A_TO_Z = ('Z' - 'A') + 1;
    // entry for invalid country codes
    private static final int INVALID_COUNTRY_ENTRY = 0x0000007F;
    // entry for countries without currency
    private static final int COUNTRY_WITHOUT_CURRENCY_ENTRY = 0x00000200;
    // mask for simple case country entries
    private static final int SIMPLE_CASE_COUNTRY_MASK = 0x00000000;
    // mask for simple case country entry final character
    private static final int SIMPLE_CASE_COUNTRY_FINAL_CHAR_MASK = 0x0000001F;
    // mask for simple case country entry default currency digits
    private static final int SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_MASK = 0x000001E0;
    // shift count for simple case country entry default currency digits
    private static final int SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_SHIFT = 5;
    // maximum number for simple case country entry default currency digits
    private static final int SIMPLE_CASE_COUNTRY_MAX_DEFAULT_DIGITS = 9;
    // mask for special case country entries
    private static final int SPECIAL_CASE_COUNTRY_MASK = 0x00000200;
    // mask for special case country index
    private static final int SPECIAL_CASE_COUNTRY_INDEX_MASK = 0x0000001F;
    // delta from entry index component in main table to index into special case tables
    private static final int SPECIAL_CASE_COUNTRY_INDEX_DELTA = 1;
    // mask for distinguishing simple and special case countries
    private static final int COUNTRY_TYPE_MASK = SIMPLE_CASE_COUNTRY_MASK | SPECIAL_CASE_COUNTRY_MASK;
    // mask for the numeric code of the currency
    private static final int NUMERIC_CODE_MASK = 0x000FFC00;
    // shift count for the numeric code of the currency
    private static final int NUMERIC_CODE_SHIFT = 10;

    // generated data
    private static int[] mainTable = new int[A_TO_Z * A_TO_Z];

    private static final int maxSpecialCases = 30;
    private static int specialCaseCount = 0;
    private static long[] specialCaseCutOverTimes = new long[maxSpecialCases];
    private static String[] specialCaseOldCurrencies = new String[maxSpecialCases];
    private static String[] specialCaseNewCurrencies = new String[maxSpecialCases];
    private static int[] specialCaseOldCurrenciesDefaultFractionDigits = new int[maxSpecialCases];
    private static int[] specialCaseNewCurrenciesDefaultFractionDigits = new int[maxSpecialCases];
    private static int[] specialCaseOldCurrenciesNumericCode = new int[maxSpecialCases];
    private static int[] specialCaseNewCurrenciesNumericCode = new int[maxSpecialCases];

    private static final int maxOtherCurrencies = 128;
    private static int otherCurrenciesCount = 0;
    private static String[] otherCurrencies = new String[maxOtherCurrencies];
    private static int[] otherCurrenciesDefaultFractionDigits = new int[maxOtherCurrencies];
    private static int[] otherCurrenciesNumericCode= new int[maxOtherCurrencies];

    // date format for parsing cut-over times
    private static SimpleDateFormat format;

    // Minor Units
    private static String[] currenciesWithDefinedMinorUnitDecimals =
        new String[SIMPLE_CASE_COUNTRY_MAX_DEFAULT_DIGITS + 1];
    private static String currenciesWithMinorUnitsUndefined;

    public static void main(String[] args) {

        // Look for "-o outputfilename" option
        if ( args.length == 2 && args[0].equals("-o") ) {
            try {
                out = new DataOutputStream(new FileOutputStream(args[1]));
            } catch ( FileNotFoundException e ) {
                System.err.println("Error: " + e.getMessage());
                e.printStackTrace(System.err);
                System.exit(1);
            }
        } else {
            System.err.println("Error: Illegal arg count");
            System.exit(1);
        }

        format = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.US);
        format.setTimeZone(TimeZone.getTimeZone("GMT"));
        format.setLenient(false);

        try {
            readInput();
            buildMainAndSpecialCaseTables();
            buildOtherTables();
            writeOutput();
            out.flush();
            out.close();
        } catch (Exception e) {
            System.err.println("Error: " + e.getMessage());
            e.printStackTrace(System.err);
            System.exit(1);
        }
    }

    private static void readInput() throws IOException {
        currencyData = new Properties();
        currencyData.load(System.in);

        // initialize other lookup strings
        formatVersion = (String) currencyData.get("formatVersion");
        dataVersion = (String) currencyData.get("dataVersion");
        validCurrencyCodes = (String) currencyData.get("all");
        for (int i = 0; i <= SIMPLE_CASE_COUNTRY_MAX_DEFAULT_DIGITS; i++) {
            currenciesWithDefinedMinorUnitDecimals[i]
                = (String) currencyData.get("minor"+i);
        }
        currenciesWithMinorUnitsUndefined  = (String) currencyData.get("minorUndefined");
        if (formatVersion == null ||
                dataVersion == null ||
                validCurrencyCodes == null ||
                currenciesWithMinorUnitsUndefined == null) {
            throw new NullPointerException("not all required data is defined in input");
        }
    }

    private static void buildMainAndSpecialCaseTables() throws Exception {
        for (int first = 0; first < A_TO_Z; first++) {
            for (int second = 0; second < A_TO_Z; second++) {
                char firstChar = (char) ('A' + first);
                char secondChar = (char) ('A' + second);
                String countryCode = (new StringBuffer()).append(firstChar).append(secondChar).toString();
                String currencyInfo = (String) currencyData.get(countryCode);
                int tableEntry = 0;
                if (currencyInfo == null) {
                    // no entry -> must be invalid ISO 3166 country code
                    tableEntry = INVALID_COUNTRY_ENTRY;
                } else {
                    int length = currencyInfo.length();
                    if (length == 0) {
                        // special case: country without currency
                       tableEntry = COUNTRY_WITHOUT_CURRENCY_ENTRY;
                    } else if (length == 3) {
                        // valid currency
                        if (currencyInfo.charAt(0) == firstChar && currencyInfo.charAt(1) == secondChar) {
                            checkCurrencyCode(currencyInfo);
                            int digits = getDefaultFractionDigits(currencyInfo);
                            if (digits < 0 || digits > SIMPLE_CASE_COUNTRY_MAX_DEFAULT_DIGITS) {
                                throw new RuntimeException("fraction digits out of range for " + currencyInfo);
                            }
                            int numericCode= getNumericCode(currencyInfo);
                            if (numericCode < 0 || numericCode >= 1000 ) {
                                throw new RuntimeException("numeric code out of range for " + currencyInfo);
                            }
                            tableEntry = SIMPLE_CASE_COUNTRY_MASK
                                    | (currencyInfo.charAt(2) - 'A')
                                    | (digits << SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_SHIFT)
                                    | (numericCode << NUMERIC_CODE_SHIFT);
                        } else {
                            tableEntry = SPECIAL_CASE_COUNTRY_MASK | (makeSpecialCaseEntry(currencyInfo) + SPECIAL_CASE_COUNTRY_INDEX_DELTA);
                        }
                    } else {
                        tableEntry = SPECIAL_CASE_COUNTRY_MASK | (makeSpecialCaseEntry(currencyInfo) + SPECIAL_CASE_COUNTRY_INDEX_DELTA);
                    }
                }
                mainTable[first * A_TO_Z + second] = tableEntry;
            }
        }
    }

    private static int getDefaultFractionDigits(String currencyCode) {
        for (int i = 0; i <= SIMPLE_CASE_COUNTRY_MAX_DEFAULT_DIGITS; i++) {
            if (Objects.nonNull(currenciesWithDefinedMinorUnitDecimals[i]) &&
                currenciesWithDefinedMinorUnitDecimals[i].indexOf(currencyCode) != -1) {
                return i;
            }
        }

        if (currenciesWithMinorUnitsUndefined.indexOf(currencyCode) != -1) {
            return -1;
        } else {
            return 2;
        }
    }

    private static int getNumericCode(String currencyCode) {
        int index = validCurrencyCodes.indexOf(currencyCode);
        String numericCode = validCurrencyCodes.substring(index + 3, index + 6);
        return Integer.parseInt(numericCode);
    }

    static HashMap<String, Integer> specialCaseMap = new HashMap<>();

    private static int makeSpecialCaseEntry(String currencyInfo) throws Exception {
        Integer oldEntry = specialCaseMap.get(currencyInfo);
        if (oldEntry != null) {
            return oldEntry.intValue();
        }
        if (specialCaseCount == maxSpecialCases) {
            throw new RuntimeException("too many special cases");
        }
        if (currencyInfo.length() == 3) {
            checkCurrencyCode(currencyInfo);
            specialCaseCutOverTimes[specialCaseCount] = Long.MAX_VALUE;
            specialCaseOldCurrencies[specialCaseCount] = currencyInfo;
            specialCaseOldCurrenciesDefaultFractionDigits[specialCaseCount] = getDefaultFractionDigits(currencyInfo);
            specialCaseOldCurrenciesNumericCode[specialCaseCount] = getNumericCode(currencyInfo);
            specialCaseNewCurrencies[specialCaseCount] = null;
            specialCaseNewCurrenciesDefaultFractionDigits[specialCaseCount] = 0;
            specialCaseNewCurrenciesNumericCode[specialCaseCount] = 0;
        } else {
            int length = currencyInfo.length();
            if (currencyInfo.charAt(3) != ';' ||
                    currencyInfo.charAt(length - 4) != ';') {
                throw new RuntimeException("invalid currency info: " + currencyInfo);
            }
            String oldCurrency = currencyInfo.substring(0, 3);
            String newCurrency = currencyInfo.substring(length - 3, length);
            checkCurrencyCode(oldCurrency);
            checkCurrencyCode(newCurrency);
            String timeString = currencyInfo.substring(4, length - 4);
            long time = format.parse(timeString).getTime();
            if (Math.abs(time - System.currentTimeMillis()) > ((long) 10) * 365 * 24 * 60 * 60 * 1000) {
                throw new RuntimeException("time is more than 10 years from present: " + time);
            }
            specialCaseCutOverTimes[specialCaseCount] = time;
            specialCaseOldCurrencies[specialCaseCount] = oldCurrency;
            specialCaseOldCurrenciesDefaultFractionDigits[specialCaseCount] = getDefaultFractionDigits(oldCurrency);
            specialCaseOldCurrenciesNumericCode[specialCaseCount] = getNumericCode(oldCurrency);
            specialCaseNewCurrencies[specialCaseCount] = newCurrency;
            specialCaseNewCurrenciesDefaultFractionDigits[specialCaseCount] = getDefaultFractionDigits(newCurrency);
            specialCaseNewCurrenciesNumericCode[specialCaseCount] = getNumericCode(newCurrency);
        }
        specialCaseMap.put(currencyInfo, new Integer(specialCaseCount));
        return specialCaseCount++;
    }

    private static void buildOtherTables() {
        if (validCurrencyCodes.length() % 7 != 6) {
            throw new RuntimeException("\"all\" entry has incorrect size");
        }
        for (int i = 0; i < (validCurrencyCodes.length() + 1) / 7; i++) {
            if (i > 0 && validCurrencyCodes.charAt(i * 7 - 1) != '-') {
                throw new RuntimeException("incorrect separator in \"all\" entry");
            }
            String currencyCode = validCurrencyCodes.substring(i * 7, i * 7 + 3);
            int numericCode = Integer.parseInt(
                validCurrencyCodes.substring(i * 7 + 3, i * 7 + 6));
            checkCurrencyCode(currencyCode);
            int tableEntry = mainTable[(currencyCode.charAt(0) - 'A') * A_TO_Z + (currencyCode.charAt(1) - 'A')];

            // Do not allow a future currency to be classified as an otherCurrency,
            // otherwise it will leak out into Currency:getAvailableCurrencies
            boolean futureCurrency = Arrays.asList(specialCaseNewCurrencies).contains(currencyCode);
            boolean simpleCurrency = (tableEntry & SIMPLE_CASE_COUNTRY_FINAL_CHAR_MASK) == (currencyCode.charAt(2) - 'A');

            // If neither a simple currency, or one defined in the future
            // then the current currency is applicable to be added to the otherTable
            if (!futureCurrency && !simpleCurrency) {
                if (otherCurrenciesCount == maxOtherCurrencies) {
                    throw new RuntimeException("too many other currencies");
                }
                otherCurrencies[otherCurrenciesCount] = currencyCode;
                otherCurrenciesDefaultFractionDigits[otherCurrenciesCount] = getDefaultFractionDigits(currencyCode);
                otherCurrenciesNumericCode[otherCurrenciesCount] = getNumericCode(currencyCode);
                otherCurrenciesCount++;
            }
        }
    }

    private static void checkCurrencyCode(String currencyCode) {
        if (currencyCode.length() != 3) {
            throw new RuntimeException("illegal length for currency code: " + currencyCode);
        }
        for (int i = 0; i < 3; i++) {
            char aChar = currencyCode.charAt(i);
            if ((aChar < 'A' || aChar > 'Z') && !currencyCode.equals("XB5")) {
                throw new RuntimeException("currency code contains illegal character: " + currencyCode);
            }
        }
        if (validCurrencyCodes.indexOf(currencyCode) == -1) {
            throw new RuntimeException("currency code not listed as valid: " + currencyCode);
        }
    }

    private static void writeOutput() throws IOException {
        out.writeInt(MAGIC_NUMBER);
        out.writeInt(Integer.parseInt(formatVersion));
        out.writeInt(Integer.parseInt(dataVersion));
        writeIntArray(mainTable, mainTable.length);
        out.writeInt(specialCaseCount);
        writeSpecialCaseEntries();
        out.writeInt(otherCurrenciesCount);
        writeOtherCurrencies();
    }

    private static void writeIntArray(int[] ia, int count) throws IOException {
        for (int i = 0; i < count; i++) {
            out.writeInt(ia[i]);
        }
    }

    private static void writeSpecialCaseEntries() throws IOException {
        for (int index = 0; index < specialCaseCount; index++) {
            out.writeLong(specialCaseCutOverTimes[index]);
            String str = (specialCaseOldCurrencies[index] != null)
                    ? specialCaseOldCurrencies[index] : "";
            out.writeUTF(str);
            str = (specialCaseNewCurrencies[index] != null)
                    ? specialCaseNewCurrencies[index] : "";
            out.writeUTF(str);
            out.writeInt(specialCaseOldCurrenciesDefaultFractionDigits[index]);
            out.writeInt(specialCaseNewCurrenciesDefaultFractionDigits[index]);
            out.writeInt(specialCaseOldCurrenciesNumericCode[index]);
            out.writeInt(specialCaseNewCurrenciesNumericCode[index]);
        }
    }

    private static void writeOtherCurrencies() throws IOException {
        for (int index = 0; index < otherCurrenciesCount; index++) {
            String str = (otherCurrencies[index] != null)
                    ? otherCurrencies[index] : "";
            out.writeUTF(str);
            out.writeInt(otherCurrenciesDefaultFractionDigits[index]);
            out.writeInt(otherCurrenciesNumericCode[index]);
        }
    }

}
