/*
 * Copyright 2014-2015 Groupon, Inc
 * Copyright 2014-2015 The Billing Project, LLC
 *
 * The Billing Project licenses this file to you 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 org.killbill.billing.catalog.plugin;

import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import javax.annotation.Nullable;

import org.killbill.billing.catalog.DefaultDuration;
import org.killbill.billing.catalog.DefaultFixed;
import org.killbill.billing.catalog.DefaultInternationalPrice;
import org.killbill.billing.catalog.DefaultPlan;
import org.killbill.billing.catalog.DefaultPlanPhase;
import org.killbill.billing.catalog.DefaultPrice;
import org.killbill.billing.catalog.DefaultPriceList;
import org.killbill.billing.catalog.DefaultPriceListSet;
import org.killbill.billing.catalog.DefaultProduct;
import org.killbill.billing.catalog.DefaultRecurring;
import org.killbill.billing.catalog.DefaultUnit;
import org.killbill.billing.catalog.DefaultUsage;
import org.killbill.billing.catalog.StandaloneCatalog;
import org.killbill.billing.catalog.api.BillingMode;
import org.killbill.billing.catalog.api.CurrencyValueNull;
import org.killbill.billing.catalog.api.Duration;
import org.killbill.billing.catalog.api.Fixed;
import org.killbill.billing.catalog.api.InternationalPrice;
import org.killbill.billing.catalog.api.Plan;
import org.killbill.billing.catalog.api.PlanPhase;
import org.killbill.billing.catalog.api.Price;
import org.killbill.billing.catalog.api.PriceList;
import org.killbill.billing.catalog.api.Product;
import org.killbill.billing.catalog.api.Recurring;
import org.killbill.billing.catalog.api.Unit;
import org.killbill.billing.catalog.api.rules.Case;
import org.killbill.billing.catalog.api.rules.CaseBillingAlignment;
import org.killbill.billing.catalog.api.rules.CaseCancelPolicy;
import org.killbill.billing.catalog.api.rules.CaseChange;
import org.killbill.billing.catalog.api.rules.CaseChangePlanAlignment;
import org.killbill.billing.catalog.api.rules.CaseChangePlanPolicy;
import org.killbill.billing.catalog.api.rules.CaseCreateAlignment;
import org.killbill.billing.catalog.api.rules.CasePhase;
import org.killbill.billing.catalog.api.rules.CasePriceList;
import org.killbill.billing.catalog.api.rules.PlanRules;
import org.killbill.billing.catalog.plugin.api.StandalonePluginCatalog;
import org.killbill.billing.catalog.rules.DefaultCaseBillingAlignment;
import org.killbill.billing.catalog.rules.DefaultCaseCancelPolicy;
import org.killbill.billing.catalog.rules.DefaultCaseChange;
import org.killbill.billing.catalog.rules.DefaultCaseChangePlanAlignment;
import org.killbill.billing.catalog.rules.DefaultCaseChangePlanPolicy;
import org.killbill.billing.catalog.rules.DefaultCaseCreateAlignment;
import org.killbill.billing.catalog.rules.DefaultCasePhase;
import org.killbill.billing.catalog.rules.DefaultCasePriceList;
import org.killbill.billing.catalog.rules.DefaultCaseStandardNaming;
import org.killbill.billing.catalog.rules.DefaultPlanRules;

import com.google.common.base.Function;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;

public class StandaloneCatalogMapper {

    private final String catalogName;
    private final BillingMode recurringBillingMode;

    private Iterable<Product> tmpDefaultProducts;
    private Iterable<Plan> tmpDefaultPlans;
    private DefaultPriceListSet tmpDefaultPriceListSet;
    private Map<String, DefaultPriceList> tmpDefaultPriceListMap;

    public StandaloneCatalogMapper(final String catalogName, final BillingMode recurringBillingMode) {
        this.catalogName = catalogName;
        this.recurringBillingMode = recurringBillingMode;
        this.tmpDefaultProducts = null;
        this.tmpDefaultPlans = null;
        this.tmpDefaultPriceListMap = new HashMap<String, DefaultPriceList>();
    }

    public StandaloneCatalog toStandaloneCatalog(final StandalonePluginCatalog pluginCatalog, @Nullable URI catalogURI) {


        final StandaloneCatalog result = new StandaloneCatalog();
        result.setCatalogName(catalogName);
        result.setEffectiveDate(pluginCatalog.getEffectiveDate().toDate());
        result.setProducts(toDefaultProducts(pluginCatalog.getProducts()));
        result.setPlans(toDefaultPlans(pluginCatalog.getPlans()));
        result.setPriceLists(toDefaultPriceListSet(pluginCatalog.getDefaultPriceList(), pluginCatalog.getChildrenPriceList()));
        result.setRecurringBillingMode(recurringBillingMode);
        result.setSupportedCurrencies(toArray(pluginCatalog.getCurrencies()));
        result.setUnits(toDefaultUnits(pluginCatalog.getUnits()));
        result.setPlanRules(toDefaultPlanRules(pluginCatalog.getPlanRules()));
        for (final Product cur : pluginCatalog.getProducts()) {
            for (Product target :  result.getCurrentProducts()) {
                if (target.getName().equals(cur.getName())) {
                    ((DefaultProduct) target).setAvailable(toFilteredDefaultProduct(cur.getAvailable()));
                    ((DefaultProduct) target).setIncluded(toFilteredDefaultProduct(cur.getIncluded()));
                    break;
                }
            }
        }
        result.initialize(result, catalogURI);
        return result;
    }


    private DefaultPlanRules toDefaultPlanRules(final PlanRules input) {
        final DefaultPlanRules result = new DefaultPlanRules();
        result.setBillingAlignmentCase(toDefaultCaseBillingAlignments(input.getCaseBillingAlignment()));
        result.setCancelCase(toDefaultCaseCancelPolicys(input.getCaseCancelPolicy()));
        result.setChangeAlignmentCase(toDefaultCaseChangePlanAlignments(input.getCaseChangePlanAlignment()));
        result.setChangeCase(toDefaultCaseChangePlanPolicies(input.getCaseChangePlanPolicy()));
        result.setCreateAlignmentCase(toDefaultCaseCreateAlignments(input.getCaseCreateAlignment()));
        result.setPriceListCase(toDefaultCasePriceLists(input.getCasePriceList()));
        return result;
    }

    final DefaultCaseChangePlanPolicy[] toDefaultCaseChangePlanPolicies(final Iterable<CaseChangePlanPolicy> input) {
        return toArrayWithTransform(input, new Function<CaseChangePlanPolicy, DefaultCaseChangePlanPolicy>() {
            @Override
            public DefaultCaseChangePlanPolicy apply(final CaseChangePlanPolicy input) {
                return toDefaultCaseChangePlanPolicy(input);
            }
        }, true);
    }

    final DefaultCaseChangePlanAlignment[] toDefaultCaseChangePlanAlignments(final Iterable<CaseChangePlanAlignment> input) {
        return toArrayWithTransform(input, new Function<CaseChangePlanAlignment, DefaultCaseChangePlanAlignment>() {
            @Override
            public DefaultCaseChangePlanAlignment apply(final CaseChangePlanAlignment input) {
                return toDefaultCaseChangePlanAlignment(input);
            }
        }, true);
    }

    final DefaultCaseBillingAlignment[] toDefaultCaseBillingAlignments(final Iterable<CaseBillingAlignment> input) {
        return toArrayWithTransform(input, new Function<CaseBillingAlignment, DefaultCaseBillingAlignment>() {
            @Override
            public DefaultCaseBillingAlignment apply(final CaseBillingAlignment input) {
                return toDefaultCaseBillingAlignment(input);
            }
        }, true);
    }

    final DefaultCaseCancelPolicy[] toDefaultCaseCancelPolicys(final Iterable<CaseCancelPolicy> input) {
        return toArrayWithTransform(input, new Function<CaseCancelPolicy, DefaultCaseCancelPolicy>() {
            @Override
            public DefaultCaseCancelPolicy apply(final CaseCancelPolicy input) {
                return toDefaultCaseCancelPolicy(input);
            }
        }, true);
    }

    final DefaultCaseCreateAlignment[] toDefaultCaseCreateAlignments(final Iterable<CaseCreateAlignment> input) {
        return toArrayWithTransform(input, new Function<CaseCreateAlignment, DefaultCaseCreateAlignment>() {
            @Override
            public DefaultCaseCreateAlignment apply(final CaseCreateAlignment input) {
                return toCaseCreateAlignment(input);
            }
        }, true);
    }

    final DefaultCasePriceList[] toDefaultCasePriceLists(final Iterable<CasePriceList> input) {
        return toArrayWithTransform(input, new Function<CasePriceList, DefaultCasePriceList>() {
            @Override
            public DefaultCasePriceList apply(final CasePriceList input) {
                return toDefaultCasePriceList(input);
            }
        }, true);
    }

    final DefaultCasePriceList toDefaultCasePriceList(final CasePriceList input) {
        final DefaultCasePriceList result = new DefaultCasePriceList();
        result.setToPriceList(toDefaultPriceList(input.getDestinationPriceList()));
        populateDefaultCase(input, result);
        return result;
    }

    final DefaultCaseCreateAlignment toCaseCreateAlignment(final CaseCreateAlignment input) {
        final DefaultCaseCreateAlignment result = new DefaultCaseCreateAlignment();
        result.setAlignment(input.getPlanAlignmentCreate());
        populateDefaultCase(input, result);
        return result;
    }

    final DefaultCaseBillingAlignment toDefaultCaseBillingAlignment(final CaseBillingAlignment input) {
        final DefaultCaseBillingAlignment result = new DefaultCaseBillingAlignment();
        result.setAlignment(input.getBillingAlignment());
        populateDefaultCasePhase(input, result);
        return result;
    }

    final DefaultCaseCancelPolicy toDefaultCaseCancelPolicy(final CaseCancelPolicy input) {
        final DefaultCaseCancelPolicy result = new DefaultCaseCancelPolicy();
        result.setPolicy(input.getBillingActionPolicy());
        populateDefaultCasePhase(input, result);
        return result;
    }

    final void populateDefaultCasePhase(final CasePhase input, final DefaultCasePhase result) {
        result.setPhaseType(input.getPhaseType());
        populateDefaultCase(input, result);
    }

    final void populateDefaultCase(final Case input, final DefaultCaseStandardNaming result) {
        result.setBillingPeriod(input.getBillingPeriod());
        result.setPriceList(toDefaultPriceList(input.getPriceList()));
        result.setProduct(toDefaultProduct(input.getProduct()));
        result.setProductCategory(input.getProductCategory());
    }

    final DefaultCaseChangePlanPolicy toDefaultCaseChangePlanPolicy(final CaseChangePlanPolicy input) {
        final DefaultCaseChangePlanPolicy result = new DefaultCaseChangePlanPolicy();
        result.setPolicy(input.getBillingActionPolicy());
        populateDefaultCaseChange(input, result);
        return result;
    }

    final DefaultCaseChangePlanAlignment toDefaultCaseChangePlanAlignment(final CaseChangePlanAlignment input) {
        final DefaultCaseChangePlanAlignment result = new DefaultCaseChangePlanAlignment();
        result.setAlignment(input.getAlignment());
        populateDefaultCaseChange(input, result);
        return result;
    }

    final void populateDefaultCaseChange(final CaseChange input, final DefaultCaseChange result) {
        result.setPhaseType(input.getPhaseType());
        result.setFromBillingPeriod(input.getFromBillingPeriod());
        result.setFromPriceList(toDefaultPriceList(input.getFromPriceList()));
        result.setFromProduct(toDefaultProduct(input.getFromProduct()));
        result.setFromProductCategory(input.getFromProductCategory());
        result.setToBillingPeriod(input.getToBillingPeriod());
        result.setToPriceList(toDefaultPriceList(input.getToPriceList()));
        result.setToProduct(toDefaultProduct(input.getToProduct()));
        result.setToProductCategory(input.getToProductCategory());
    }


    private Iterable<Product> toDefaultProducts(final Iterable<Product> input) {
        if (tmpDefaultProducts == null) {
            final Function<Product, Product> productTransformer = new Function<Product, Product>() {
                @Override
                public Product apply(final Product input) {
                    return toDefaultProduct(input);
                }
            };
            tmpDefaultProducts = ImmutableList.copyOf(Iterables.transform(input, productTransformer));
        }
        return tmpDefaultProducts;
    }

    private Collection<Product> toFilteredDefaultProduct(final Collection<Product> input) {
        if (!input.iterator().hasNext()) {
            return Collections.emptyList();
        }
        final Iterable<String> inputProductNames = Iterables.transform(input, new Function<Product, String>() {
            @Override
            public String apply(final Product input) {
                return input.getName();
            }
        });
        final Collection<Product> filteredAndOrdered = new ArrayList<Product>(input.size());
        for (final String cur : inputProductNames) {
            final Product found = findOrIllegalState(tmpDefaultProducts, new Predicate<Product>() {
                @Override
                public boolean apply(final Product inputPredicate) {
                    return inputPredicate.getName().equals(cur);
                }
            }, "Failed to find product " + cur);
            filteredAndOrdered.add(found);
        }
        return filteredAndOrdered;
    }

    private Iterable<Plan> toDefaultPlans(final Iterable<Plan> input) {
        if (tmpDefaultPlans == null) {
            final Function<Plan, Plan> planTransformer = new Function<Plan, Plan>() {
                @Override
                public Plan apply(final Plan input) {
                    return toDefaultPlan(input);
                }
            };
            tmpDefaultPlans = ImmutableList.copyOf(Iterables.transform(input, planTransformer));
        }
        return tmpDefaultPlans;
    }

    private Iterable<Plan> toFilterDefaultPlans(final String priceListName) {
        if (tmpDefaultPlans == null) {
            throw new IllegalStateException("Cannot filter on uninitialized plans");
        }
        return Iterables.filter(tmpDefaultPlans, new Predicate<Plan>() {
            @Override
            public boolean apply(final Plan input) {
                return input.getPriceListName().equals(priceListName);
            }
        });
    }

    private DefaultPriceListSet toDefaultPriceListSet(final PriceList defaultPriceList, final Iterable<PriceList> childrenPriceLists) {
        if (tmpDefaultPriceListSet == null) {
            tmpDefaultPriceListSet = new DefaultPriceListSet(toDefaultPriceList(defaultPriceList), toDefaultPriceLists(childrenPriceLists));
        }
        return tmpDefaultPriceListSet;
    }

    private DefaultPlanPhase[] toDefaultPlanPhases(final Iterable<PlanPhase> input) {
        if (!input.iterator().hasNext()) {
            return new DefaultPlanPhase[0];
        }
        return toArrayWithTransform(input, new Function<PlanPhase, DefaultPlanPhase>() {
            @Override
            public DefaultPlanPhase apply(final PlanPhase input) {
                return toDefaultPlanPhase(input);
            }
        }, false);
    }

    private DefaultPriceList[] toDefaultPriceLists(final Iterable<PriceList> input) {
        return toArrayWithTransform(input, new Function<PriceList, DefaultPriceList>() {
            @Override
            public DefaultPriceList apply(final PriceList input) {
                return toDefaultPriceList(input);
            }
        }, true);
    }

    private DefaultPrice[] toDefaultPrices(final Iterable<Price> input) {
        return toArrayWithTransform(input, new Function<Price, DefaultPrice>() {
            @Override
            public DefaultPrice apply(final Price input) {
                return toDefaultPrice(input);
            }
        }, false);
    }

    private DefaultUnit[] toDefaultUnits(final Iterable<Unit> input) {
        return toArrayWithTransform(input, new Function<Unit, DefaultUnit>() {
            @Override
            public DefaultUnit apply(final Unit inputTransform) {
                return toDefaultUnit(inputTransform);
            }
        }, true);
    }

    private DefaultUnit toDefaultUnit(final Unit input) {
        final DefaultUnit result = new DefaultUnit();
        result.setName(input.getName());
        return result;
    }


    private DefaultPriceList toDefaultPriceList(@Nullable final PriceList input) {
        if (input == null) {
            return null;
        }

        DefaultPriceList result = tmpDefaultPriceListMap.get(input.getName());
        if (result == null) {
            result = new DefaultPriceList();
            result.setName(input.getName());
            result.setPlans(toFilterDefaultPlans(input.getName()));
            tmpDefaultPriceListMap.put(input.getName(), result);
        }
        return result;
    }


    private Product toDefaultProduct(@Nullable final Product input) {
        if (input == null) {
            return null;
        }
        if (tmpDefaultProducts != null) {
            final Product existingProduct = findOrIllegalState(tmpDefaultProducts, new Predicate<Product>() {
                @Override
                public boolean apply(final Product predicateInput) {
                    return predicateInput.getName().equals(input.getName());
                }
            }, "Unknown product " + input.getName());
            return existingProduct;
        }
        final DefaultProduct result = new DefaultProduct();
        result.setCatalogName(catalogName);
        result.setCatagory(input.getCategory());
        result.setName(input.getName());
        return result;
    }

    private Plan toDefaultPlan(final Plan input) {
        if (tmpDefaultPlans != null) {
            final Plan existingPlan = findOrIllegalState(tmpDefaultPlans, new Predicate<Plan>() {
                @Override
                public boolean apply(final Plan predicateInput) {
                    return predicateInput.getName().equals(input.getName());
                }
            }, "Unknown plan " + input.getName());
            return existingPlan;
        }
        final DefaultPlan result = new DefaultPlan();
        result.setName(input.getName());
        result.setEffectiveDateForExistingSubscriptions(input.getEffectiveDateForExistingSubscriptions());
        result.setFinalPhase(toDefaultPlanPhase(input.getFinalPhase()));
        result.setInitialPhases(toDefaultPlanPhases(ImmutableList.copyOf(input.getInitialPhases())));
        result.setPlansAllowedInBundle(input.getPlansAllowedInBundle());
        result.setProduct(toDefaultProduct(input.getProduct()));
        result.setPriceListName(input.getPriceListName());
        return result;
    }

    private DefaultPlanPhase toDefaultPlanPhase(final PlanPhase input) {
        final DefaultPlanPhase result = new DefaultPlanPhase();
        result.setDuration(toDefaultDuration(input.getDuration()));
        result.setFixed(toDefaultFixed(input.getFixed()));
        result.setPhaseType(input.getPhaseType());
        result.setRecurring(toDefaultRecurring(input.getRecurring()));
        result.setUsages(new DefaultUsage[0]);
        return result;
    }

    private DefaultRecurring toDefaultRecurring(final Recurring input) {
        DefaultRecurring result = null;
        if (input != null) {
            result = new DefaultRecurring();
            result.setBillingPeriod(input.getBillingPeriod());
            result.setRecurringPrice(toDefaultInternationalPrice(input.getRecurringPrice()));
        }
        return result;
    }

    private final DefaultDuration toDefaultDuration(final Duration input) {
        final DefaultDuration result = new DefaultDuration();
        result.setNumber(input.getNumber());
        result.setUnit(input.getUnit());
        return result;
    }

    private final DefaultFixed toDefaultFixed(@Nullable final Fixed input) {
        DefaultFixed result = null;
        if (input != null) {
            result = new DefaultFixed();
            result.setFixedPrice(toDefaultInternationalPrice(input.getPrice()));
            result.setType(input.getType());
        }
        return result;
    }

    private DefaultInternationalPrice toDefaultInternationalPrice(final InternationalPrice input) {
        final DefaultInternationalPrice result = new DefaultInternationalPrice();
        result.setPrices(toDefaultPrices(ImmutableList.copyOf(input.getPrices())));
        return result;
    }

    private DefaultPrice toDefaultPrice(final Price input) {
        try {
            final DefaultPrice result = new DefaultPrice();
            result.setCurrency(input.getCurrency());
            result.setValue(input.getValue());
            return result;
        } catch (CurrencyValueNull currencyValueNull) {
            throw new IllegalStateException(currencyValueNull);
        }
    }

    private <I, C extends I> C[] toArrayWithTransform(final Iterable<I> input, final Function<I, C> transformer, boolean returnNullIfNothing) {
        if (returnNullIfNothing && (input == null || !input.iterator().hasNext())) {
            return null;
        }
        final Iterable<C> tmp = Iterables.transform(input, transformer);
        return toArray(tmp);
    }

    private <C> C[] toArray(final Iterable<C> input) {
        if (!input.iterator().hasNext()) {
            throw new IllegalStateException("Nothing to convert into array");
        }
        final C[] foo = (C[]) java.lang.reflect.Array
                .newInstance(input.iterator().next().getClass(), 1);
        return ImmutableList.<C>copyOf(input).toArray(foo);
    }

    private <T> T findOrIllegalState(final Iterable<T> input, final Predicate<T> predicate, final String msg) {
        T result = Iterables.<T> tryFind(input, predicate).orNull();
        if (result == null) {
            throw new IllegalStateException(msg);
        }
        return result;
    }
}