/*
 * Copyright (c) 2012, 2018, Werner Keil, Anatole Tresch and others.
 *
 * 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.
 *
 * Contributors: @atsticks, @keilw, @otjava
 */
package org.javamoney.cdi.internal;

import org.javamoney.cdi.Amount;
import org.javamoney.cdi.AmountConversion;
import org.javamoney.cdi.AmountCurrency;
import org.javamoney.cdi.AmountFormat;

import javax.enterprise.context.Dependent;
import javax.enterprise.inject.Produces;
import javax.enterprise.inject.spi.InjectionPoint;
import javax.money.*;
import javax.money.convert.*;
import javax.money.format.AmountFormatQuery;
import javax.money.format.AmountFormatQueryBuilder;
import javax.money.format.MonetaryAmountFormat;
import javax.money.format.MonetaryFormats;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Producer bean that bridges to the javamoney API.
 */
public final class MonetaryProducer {

    private static final Logger LOG = Logger.getLogger(MonetaryProducer.class.getName());

    /**
     * Class is not instanciatable.
     */
    private MonetaryProducer(){}

    @Produces @Dependent
    public static CurrencyUnit currencyUnit(InjectionPoint ip){
        AmountCurrency specAnnot = ip.getAnnotated()!=null?
                ip.getAnnotated().getAnnotation(AmountCurrency.class):
                null;
        if(specAnnot==null){
            java.util.Currency jdkDefault = java.util.Currency.getInstance(Locale.getDefault());
            if(jdkDefault!=null) {
                return Monetary.getCurrency(jdkDefault.getCurrencyCode());
            }else{
                throw new MonetaryException("No default currency found.");
            }
        }
        return Monetary.getCurrency(createCurrencyQuery(specAnnot));
    }


    @Produces @Dependent
    public static Collection<CurrencyUnit> currencyUnits(InjectionPoint ip){
        AmountCurrency specAnnot = ip.getAnnotated()!=null?ip.getAnnotated().getAnnotation(AmountCurrency.class):null;
        if(specAnnot!=null){
            return Monetary.getCurrencies(createCurrencyQuery(specAnnot));
        }
        return Monetary.getCurrencies();
    }


    @Produces @Dependent
    public static ExchangeRateProvider rateProvider(InjectionPoint ip){
        AmountConversion specAnnot = ip.getAnnotated()!=null?ip.getAnnotated().getAnnotation(AmountConversion.class):null;
        if(specAnnot!=null){
            return MonetaryConversions.getExchangeRateProvider(createConversionQuery(specAnnot));
        }
        return MonetaryConversions.getExchangeRateProvider();
    }

    @Produces @Dependent
    public static Collection<ExchangeRateProvider> rateProviders(InjectionPoint ip){
        List<ExchangeRateProvider> providers = new ArrayList<>();
        AmountConversion specAnnot = ip.getAnnotated()!=null?ip.getAnnotated().getAnnotation(AmountConversion.class):null;
        if(specAnnot!=null && specAnnot.providers().length>0){
            for(String providerId:specAnnot.providers()){
                providers.add(MonetaryConversions.getExchangeRateProvider(providerId));
            }
        }else{
            for(String providerId:MonetaryConversions.getConversionProviderNames()){
                providers.add(MonetaryConversions.getExchangeRateProvider(providerId));
            }
        }
        return providers;
    }


    @Produces @Dependent
    public static CurrencyConversion currencyConversion(InjectionPoint ip){
        AmountConversion specAnnot = ip.getAnnotated()!=null?ip.getAnnotated().getAnnotation(AmountConversion.class):null;
        if(specAnnot==null){
            throw new IllegalArgumentException("@RateSpec is required.");
        }
        return MonetaryConversions.getConversion(createConversionQuery(specAnnot));
    }

    @Produces @Dependent
    public static ExchangeRate exchangeRate(InjectionPoint ip){
        AmountConversion specAnnot = ip.getAnnotated()!=null?ip.getAnnotated().getAnnotation(AmountConversion.class):null;
        if(specAnnot==null){
            throw new IllegalArgumentException("@RateSpec is required.");
        }
        ConversionQuery query = createConversionQuery(specAnnot);
        return MonetaryConversions.getExchangeRateProvider(query)
                .getExchangeRate(query);
    }

    @Produces @Dependent
    public static Collection<ExchangeRate> exchangeRates(InjectionPoint ip){
        AmountConversion specAnnot = ip.getAnnotated()!=null?ip.getAnnotated().getAnnotation(AmountConversion.class):null;
        if(specAnnot==null){
            throw new IllegalArgumentException("@RateSpec is required.");
        }
        ConversionQuery query = createConversionQuery(specAnnot);
        Collection<String> providers = query.getProviderNames();
        if(providers.isEmpty()){
            providers = MonetaryConversions.getConversionProviderNames();
        }
        List<ExchangeRate> rates = new ArrayList<>();
        for(String providerId:providers){
            ExchangeRateProvider provider = MonetaryConversions.getExchangeRateProvider(providerId);
            try {
                ExchangeRate rate = provider.getExchangeRate(query);
                if (rate != null) {
                    rates.add(rate);
                }
            }catch(Exception e){
                LOG.log(Level.FINE,
                        "Could not evaluate rate from provider '" + providerId + "': "+  query,
                        e);
            }
        }
        return rates;
    }


    @Produces @Dependent
    public static MonetaryAmountFormat amountFormat(InjectionPoint ip){
        AmountFormat specAnnot = ip.getAnnotated()!=null?ip.getAnnotated().getAnnotation(AmountFormat.class):null;
        if(specAnnot==null){
            throw new IllegalArgumentException("@FormatName is required.");
        }
        return MonetaryFormats.getAmountFormat(createAmountFormatQuery(
                specAnnot,
                ip.getAnnotated().getAnnotation(Amount.class)));
    }


    @Produces @Dependent
    public static Collection<MonetaryAmountFormat> amountFormats(InjectionPoint ip){
        AmountFormat specAnnot = ip.getAnnotated()!=null?ip.getAnnotated().getAnnotation(AmountFormat.class):null;
        if(specAnnot==null){
            throw new IllegalArgumentException("@FormatName is required.");
        }
        return MonetaryFormats.getAmountFormats(createAmountFormatQuery(
                specAnnot,
                ip.getAnnotated().getAnnotation(Amount.class)));
    }


    @Produces @Dependent
    public static MonetaryAmountFactory amountFactory(InjectionPoint ip){
        Amount specAnnot = ip.getAnnotated()!=null?ip.getAnnotated().getAnnotation(Amount.class):null;
        if(specAnnot!=null){
            return Monetary.getAmountFactory(createAmountQuery(specAnnot));
        }
        return Monetary.getDefaultAmountFactory();
    }


    @SuppressWarnings("unchecked")
    @Produces @Dependent
    public static Collection<MonetaryAmountFactory> amountFactories(InjectionPoint ip){
        Amount specAnnot = ip.getAnnotated()!=null?ip.getAnnotated().getAnnotation(Amount.class):null;
        if(specAnnot!=null){
            return Collection.class.cast(Monetary.getAmountFactories(createAmountQuery(specAnnot)));
        }
        return Collection.class.cast(Monetary.getAmountFactories());
    }

    @Produces @Dependent
    public static CurrencyQuery currencyQuery(InjectionPoint ip){
        AmountCurrency specAnnot = ip.getAnnotated()!=null?ip.getAnnotated().getAnnotation(AmountCurrency.class):null;
        if(specAnnot==null){
            return null;
        }
        return createCurrencyQuery(specAnnot);
    }

    @Produces @Dependent
    public static ConversionQuery conversionQuery(InjectionPoint ip){
        AmountConversion specAnnot = ip.getAnnotated()!=null?ip.getAnnotated().getAnnotation(AmountConversion.class):null;
        if(specAnnot==null){
            return null;
        }
        return createConversionQuery(specAnnot);
    }

    @Produces @Dependent
    public static AmountFormatQuery amountFormatQuery(InjectionPoint ip){
        AmountFormat specAnnot = ip.getAnnotated()!=null?ip.getAnnotated().getAnnotation(AmountFormat.class):null;
        if(specAnnot==null){
            return null;
        }
        Amount amountSpec = ip.getAnnotated()!=null?ip.getAnnotated().getAnnotation(Amount.class):null;
        return createAmountFormatQuery(specAnnot, amountSpec);
    }

    @Produces @Dependent
    public static MonetaryAmountFactoryQuery amountFactoryQuery(InjectionPoint ip){
        Amount amountSpec = ip.getAnnotated()!=null?ip.getAnnotated().getAnnotation(Amount.class):null;
        if(amountSpec==null){
            return null;
        }
        return createAmountQuery(amountSpec);
    }

    private static CurrencyQuery createCurrencyQuery(AmountCurrency specAnnot) {
        CurrencyQueryBuilder b = CurrencyQueryBuilder.of();
        b.setCountries(convertToCountries(specAnnot.countries()));
        b.setCurrencyCodes(specAnnot.codes());
        b.setNumericCodes(specAnnot.numericCodes());
        if(specAnnot.attributes().length>0){
            applyAttributes(specAnnot.attributes(), b);
        }
        if(specAnnot.providers().length>0){
            b.setProviderNames(specAnnot.providers());
        }
        return b.build();
    }

    private static Locale[] convertToCountries(String[] codes) {
        Locale[] locales = new Locale[codes.length];
        for(int i=0;i<codes.length;i++){
            locales[i] = new Locale("", codes[i]);
        }
        return locales;
    }

    private static ConversionQuery createConversionQuery(AmountConversion specAnnot) {
        ConversionQueryBuilder b = ConversionQueryBuilder.of();
        if(!specAnnot.baseCurrency().isEmpty()){
            b.setBaseCurrency(specAnnot.baseCurrency());
        }
        if(!specAnnot.termCurrency().isEmpty()){
            b.setTermCurrency(specAnnot.termCurrency());
        }
        if(specAnnot.attributes().length>0){
            applyAttributes(specAnnot.attributes(), b);
        }
        if(specAnnot.providers().length>0){
            b.setProviderNames(specAnnot.providers());
        }
        if(specAnnot.rateTypes().length==0){
            b.setRateTypes(specAnnot.rateTypes());
        }
        return b.build();
    }

    private static AmountFormatQuery createAmountFormatQuery(AmountFormat specAnnot, Amount amountSpec) {
        AmountFormatQueryBuilder b = AmountFormatQueryBuilder.of(specAnnot.name());
        if(specAnnot.providers().length>0){
            b.setProviderNames(specAnnot.providers());
        }
        if(specAnnot.attributes().length>0){
            applyAttributes(specAnnot.attributes(), b);
        }
        if(!specAnnot.locale().isEmpty()){
            b.setLocale(createLocale(specAnnot.locale()));
        }else{
            // Required to avoid NOE, see https://github.com/JavaMoney/jsr354-ri/issues/156
            b.setLocale(Locale.getDefault());
        }
        if(amountSpec!=null){
            b.setMonetaryAmountFactory(Monetary.getAmountFactory(createAmountQuery(amountSpec)));
        }
        return b.build();
    }

    private static Locale createLocale(String locale) {
        String[] parts = locale.split("\\_", 3);
        switch(parts.length){
            case 1:
                return new Locale(parts[0]);
            case 2:
                return new Locale(parts[0], parts[1]);
            default:
                return new Locale(parts[0], parts[1], parts[2]);
        }
    }

    private static MonetaryAmountFactoryQuery createAmountQuery(Amount specAnnot) {
        MonetaryAmountFactoryQueryBuilder b = MonetaryAmountFactoryQueryBuilder.of();
        if(specAnnot.value()!=MonetaryAmount.class){
            b.setTargetType(specAnnot.value());
        }
        if(specAnnot.providers().length>0){
            b.setProviderNames(specAnnot.providers());
        }
        if(specAnnot.attributes().length>0){
            applyAttributes(specAnnot.attributes(), b);
        }
        if(specAnnot.fixedScale()){
            b.setFixedScale(true);
        }
        if(specAnnot.maxScale()>0){
            b.setMaxScale(specAnnot.maxScale());
        }
        if(specAnnot.precision()>0){
            b.setPrecision(specAnnot.precision());
        }
        return b.build();
    }

    private static void applyAttributes(String[] attributes, AbstractQueryBuilder b) {
        for(String attr:attributes){
            String[] parts = attr.split("=", 2);
            b.set(parts[0], parts[1]);
        }
    }

}
