try {
            final SubscriptionCatalog catalog = subscriptionCatalogApi.getFullCatalog(context);
            return super.createBundleForAccount(accountId, bundleKey, renameCancelledBundleIfExist, catalog, accountIdCacheController, context);
        } catch (final CatalogApiException e) {
            throw new SubscriptionBaseApiException(e);
        }

package org.killbill.billing.subscription.api.svcs;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import javax.annotation.Nullable;
import org.joda.time.DateTime;
import org.joda.time.LocalDate;
import org.killbill.billing.ErrorCode;
import org.killbill.billing.ObjectType;
import org.killbill.billing.callcontext.InternalCallContext;
import org.killbill.billing.callcontext.InternalTenantContext;
import org.killbill.billing.catalog.api.BillingActionPolicy;
import org.killbill.billing.catalog.api.CatalogApiException;
import org.killbill.billing.catalog.api.Plan;
import org.killbill.billing.catalog.api.PlanPhasePriceOverridesWithCallContext;
import org.killbill.billing.catalog.api.Product;
import org.killbill.billing.catalog.api.ProductCategory;
import org.killbill.billing.catalog.api.StaticCatalog;
import org.killbill.billing.catalog.api.VersionedCatalog;
import org.killbill.billing.entitlement.api.Entitlement.EntitlementState;
import org.killbill.billing.entitlement.api.EntitlementAOStatusDryRun;
import org.killbill.billing.entitlement.api.EntitlementAOStatusDryRun.DryRunChangeReason;
import org.killbill.billing.entitlement.api.EntitlementSpecifier;
import org.killbill.billing.events.EffectiveSubscriptionInternalEvent;
import org.killbill.billing.invoice.api.DryRunArguments;
import org.killbill.billing.subscription.api.SubscriptionBase;
import org.killbill.billing.subscription.api.SubscriptionBaseApiService;
import org.killbill.billing.subscription.api.SubscriptionBaseInternalApi;
import org.killbill.billing.subscription.api.SubscriptionBaseWithAddOns;
import org.killbill.billing.subscription.api.SubscriptionBaseWithAddOnsSpecifier;
import org.killbill.billing.subscription.api.user.DefaultEffectiveSubscriptionEvent;
import org.killbill.billing.subscription.api.user.DefaultSubscriptionBase;
import org.killbill.billing.subscription.api.user.DefaultSubscriptionBaseBundle;
import org.killbill.billing.subscription.api.user.DefaultSubscriptionStatusDryRun;
import org.killbill.billing.subscription.api.user.SubscriptionBaseApiException;
import org.killbill.billing.subscription.api.user.SubscriptionBaseBundle;
import org.killbill.billing.subscription.api.user.SubscriptionBaseTransition;
import org.killbill.billing.subscription.api.user.SubscriptionBaseTransitionData;
import org.killbill.billing.subscription.api.user.SubscriptionBillingEvent;
import org.killbill.billing.subscription.api.user.SubscriptionBuilder;
import org.killbill.billing.subscription.catalog.DefaultSubscriptionCatalogApi;
import org.killbill.billing.subscription.catalog.SubscriptionCatalog;
import org.killbill.billing.subscription.catalog.SubscriptionCatalogApi;
import org.killbill.billing.subscription.engine.addon.AddonUtils;
import org.killbill.billing.subscription.engine.dao.SubscriptionDao;
import org.killbill.billing.subscription.engine.dao.model.SubscriptionBundleModelDao;
import org.killbill.billing.subscription.events.SubscriptionBaseEvent;
import org.killbill.billing.subscription.events.bcd.BCDEvent;
import org.killbill.billing.subscription.events.bcd.BCDEventData;
import org.killbill.billing.util.api.AuditLevel;
import org.killbill.billing.util.audit.AuditLogWithHistory;
import org.killbill.billing.util.cache.AccountIdFromBundleIdCacheLoader;
import org.killbill.billing.util.cache.BundleIdFromSubscriptionIdCacheLoader;
import org.killbill.billing.util.cache.Cachable.CacheType;
import org.killbill.billing.util.cache.CacheController;
import org.killbill.billing.util.cache.CacheControllerDispatcher;
import org.killbill.billing.util.cache.CacheLoaderArgument;
import org.killbill.billing.util.callcontext.CallContext;
import org.killbill.billing.util.callcontext.InternalCallContextFactory;
import org.killbill.billing.util.callcontext.TenantContext;
import org.killbill.billing.util.entity.Entity;
import org.killbill.billing.util.entity.Pagination;
import org.killbill.billing.util.entity.dao.DefaultPaginationHelper.SourcePaginationBuilder;
import org.killbill.clock.Clock;
import org.killbill.clock.DefaultClock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.inject.Inject;
import static org.killbill.billing.util.entity.dao.DefaultPaginationHelper.getEntityPaginationNoException;

public class DefaultSubscriptionInternalApi extends DefaultSubscriptionBaseCreateApi implements SubscriptionBaseInternalApi {

    private static final Logger log = LoggerFactory.getLogger(DefaultSubscriptionInternalApi.class);

    private final AddonUtils addonUtils;

    private final InternalCallContextFactory internalCallContextFactory;

    private final SubscriptionCatalogApi subscriptionCatalogApi;

    private final CacheController<UUID, UUID> accountIdCacheController;

    private final CacheController<UUID, UUID> bundleIdCacheController;

    public static final Comparator<SubscriptionBase> SUBSCRIPTIONS_COMPARATOR = new Comparator<SubscriptionBase>() {

        @Override
        public int compare(final SubscriptionBase o1, final SubscriptionBase o2) {
            if (o1.getCategory() == ProductCategory.BASE) {
                return -1;
            } else if (o2.getCategory() == ProductCategory.BASE) {
                return 1;
            } else {
                return ((DefaultSubscriptionBase) o1).getAlignStartDate().compareTo(((DefaultSubscriptionBase) o2).getAlignStartDate());
            }
        }
    };

    @Inject
    public DefaultSubscriptionInternalApi(final SubscriptionDao dao, final SubscriptionBaseApiService apiService, final Clock clock, final SubscriptionCatalogApi subscriptionCatalogApi, final AddonUtils addonUtils, final CacheControllerDispatcher cacheControllerDispatcher, final InternalCallContextFactory internalCallContextFactory) {
        super(dao, apiService, clock);
        this.addonUtils = addonUtils;
        this.internalCallContextFactory = internalCallContextFactory;
        this.subscriptionCatalogApi = subscriptionCatalogApi;
        this.accountIdCacheController = cacheControllerDispatcher.getCacheController(CacheType.ACCOUNT_ID_FROM_BUNDLE_ID);
        this.bundleIdCacheController = cacheControllerDispatcher.getCacheController(CacheType.BUNDLE_ID_FROM_SUBSCRIPTION_ID);
    }

    @Override
    public List<SubscriptionBaseWithAddOns> createBaseSubscriptionsWithAddOns(final VersionedCatalog publicCatalog, final Iterable<SubscriptionBaseWithAddOnsSpecifier> subscriptionWithAddOnsSpecifiers, final boolean renameCancelledBundleIfExist, final InternalCallContext context) throws SubscriptionBaseApiException {
        try {
            final SubscriptionCatalog catalog = DefaultSubscriptionCatalogApi.wrapCatalog(publicCatalog, clock);
            final CallContext callContext = internalCallContextFactory.createCallContext(context);
            return super.createBaseSubscriptionsWithAddOns(subscriptionWithAddOnsSpecifiers, renameCancelledBundleIfExist, catalog, addonUtils, accountIdCacheController, bundleIdCacheController, callContext, context);
        } catch (final CatalogApiException e) {
            throw new SubscriptionBaseApiException(e);
        }
    }

    @Override
    public void cancelBaseSubscriptions(final Iterable<SubscriptionBase> subscriptions, final BillingActionPolicy policy, final InternalCallContext context) throws SubscriptionBaseApiException {
        try {
            final SubscriptionCatalog catalog = subscriptionCatalogApi.getFullCatalog(context);
            apiService.cancelWithPolicyNoValidationAndCatalog(Iterables.<SubscriptionBase, DefaultSubscriptionBase>transform(subscriptions, new Function<SubscriptionBase, DefaultSubscriptionBase>() {

                @Override
                public DefaultSubscriptionBase apply(final SubscriptionBase subscriptionBase) {
                    try {
                        return getDefaultSubscriptionBase(subscriptionBase, catalog, context);
                    } catch (final CatalogApiException e) {
                        throw new RuntimeException(e);
                    }
                }
            }), policy, catalog, context);
        } catch (CatalogApiException e) {
            throw new SubscriptionBaseApiException(e);
        }
    }

    @VisibleForTesting
    @Override
    public SubscriptionBaseBundle createBundleForAccount(final UUID accountId, final String bundleKey, final boolean renameCancelledBundleIfExist, final InternalCallContext context) throws SubscriptionBaseApiException {
        final DateTime now = context.getCreatedDate();
        final DefaultSubscriptionBaseBundle bundle = new DefaultSubscriptionBaseBundle(bundleKey, accountId, now, now, now, now);
        if (null != bundleKey && bundleKey.length() > 255) {
            throw new SubscriptionBaseApiException(ErrorCode.EXTERNAL_KEY_LIMIT_EXCEEDED);
        }

    }

    @Override
    public List<SubscriptionBaseBundle> getBundlesForAccountAndKey(final UUID accountId, final String bundleKey, final InternalTenantContext context) throws SubscriptionBaseApiException {
        final SubscriptionBaseBundle subscriptionBundlesForAccountAndKey = dao.getSubscriptionBundlesForAccountAndKey(accountId, bundleKey, context);
        return subscriptionBundlesForAccountAndKey != null ? ImmutableList.of(subscriptionBundlesForAccountAndKey) : ImmutableList.<SubscriptionBaseBundle>of();
    }

    @Override
    public List<SubscriptionBaseBundle> getBundlesForAccount(final UUID accountId, final InternalTenantContext context) {
        return dao.getSubscriptionBundleForAccount(accountId, context);
    }

    @Override
    public List<SubscriptionBaseBundle> getBundlesForKey(final String bundleKey, final InternalTenantContext context) {
        return dao.getSubscriptionBundlesForKey(bundleKey, context);
    }

    @Override
    public Pagination<SubscriptionBaseBundle> getBundles(final Long offset, final Long limit, final InternalTenantContext context) {
        return getEntityPaginationNoException(limit, new SourcePaginationBuilder<SubscriptionBundleModelDao, SubscriptionBaseApiException>() {

            @Override
            public Pagination<SubscriptionBundleModelDao> build() {
                return dao.get(offset, limit, context);
            }
        }, new Function<SubscriptionBundleModelDao, SubscriptionBaseBundle>() {

            @Override
            public SubscriptionBaseBundle apply(final SubscriptionBundleModelDao bundleModelDao) {
                return SubscriptionBundleModelDao.toSubscriptionBundle(bundleModelDao);
            }
        });
    }

    @Override
    public Pagination<SubscriptionBaseBundle> searchBundles(final String searchKey, final Long offset, final Long limit, final InternalTenantContext context) {
        return getEntityPaginationNoException(limit, new SourcePaginationBuilder<SubscriptionBundleModelDao, SubscriptionBaseApiException>() {

            @Override
            public Pagination<SubscriptionBundleModelDao> build() {
                return dao.searchSubscriptionBundles(searchKey, offset, limit, context);
            }
        }, new Function<SubscriptionBundleModelDao, SubscriptionBaseBundle>() {

            @Override
            public SubscriptionBaseBundle apply(final SubscriptionBundleModelDao bundleModelDao) {
                return SubscriptionBundleModelDao.toSubscriptionBundle(bundleModelDao);
            }
        });
    }

    @Override
    public Iterable<UUID> getNonAOSubscriptionIdsForKey(final String bundleKey, final InternalTenantContext context) {
        return dao.getNonAOSubscriptionIdsForKey(bundleKey, context);
    }

    @Override
    public SubscriptionBaseBundle getActiveBundleForKey(final VersionedCatalog publicCatalog, final String bundleKey, final InternalTenantContext context) {
        try {
            final SubscriptionCatalog catalog = DefaultSubscriptionCatalogApi.wrapCatalog(publicCatalog, clock);
            return super.getActiveBundleForKey(bundleKey, catalog, context);
        } catch (final CatalogApiException e) {
            log.warn("Failed to get subscriptions", e);
            return null;
        }
    }

    @Override
    public List<SubscriptionBase> getSubscriptionsForBundle(final UUID bundleId, @Nullable final DryRunArguments dryRunArguments, final InternalTenantContext context) throws SubscriptionBaseApiException {
        try {
            final SubscriptionCatalog catalog = subscriptionCatalogApi.getFullCatalog(context);
            final TenantContext tenantContext = internalCallContextFactory.createTenantContext(context);
            final List<DefaultSubscriptionBase> subscriptionsForBundle = super.getSubscriptionsForBundle(bundleId, dryRunArguments, catalog, addonUtils, tenantContext, context);
            return new ArrayList<SubscriptionBase>(subscriptionsForBundle);
        } catch (final CatalogApiException e) {
            throw new SubscriptionBaseApiException(e);
        }
    }

    @Override
    public Map<UUID, List<SubscriptionBase>> getSubscriptionsForAccount(final VersionedCatalog publicCatalog, final InternalTenantContext context) throws SubscriptionBaseApiException {
        try {
            final SubscriptionCatalog catalog = DefaultSubscriptionCatalogApi.wrapCatalog(publicCatalog, clock);
            final Map<UUID, List<DefaultSubscriptionBase>> internalSubscriptions = dao.getSubscriptionsForAccount(catalog, context);
            final Map<UUID, List<SubscriptionBase>> result = new HashMap<UUID, List<SubscriptionBase>>();
            for (final UUID bundleId : internalSubscriptions.keySet()) {
                final List<DefaultSubscriptionBase> subscriptionsForApiUse = createSubscriptionsForApiUse(internalSubscriptions.get(bundleId));
                result.put(bundleId, new ArrayList<SubscriptionBase>(subscriptionsForApiUse));
            }
            return result;
        } catch (final CatalogApiException e) {
            throw new SubscriptionBaseApiException(e);
        }
    }

    @Override
    public SubscriptionBase getBaseSubscription(final UUID bundleId, final InternalTenantContext context) throws SubscriptionBaseApiException {
        try {
            final SubscriptionCatalog catalog = subscriptionCatalogApi.getFullCatalog(context);
            return super.getBaseSubscription(bundleId, catalog, context);
        } catch (final CatalogApiException e) {
            throw new SubscriptionBaseApiException(e);
        }
    }

    @Override
    public SubscriptionBase getSubscriptionFromId(final UUID id, final InternalTenantContext context) throws SubscriptionBaseApiException {
        try {
            final SubscriptionCatalog catalog = subscriptionCatalogApi.getFullCatalog(context);
            final SubscriptionBase result = dao.getSubscriptionFromId(id, catalog, context);
            if (result == null) {
                throw new SubscriptionBaseApiException(ErrorCode.SUB_INVALID_SUBSCRIPTION_ID, id);
            }
            return createSubscriptionForApiUse(result);
        } catch (final CatalogApiException e) {
            throw new SubscriptionBaseApiException(e);
        }
    }

    @Override
    public SubscriptionBase getSubscriptionFromExternalKey(final String externalKey, final InternalTenantContext context) throws SubscriptionBaseApiException {
        try {
            final SubscriptionCatalog catalog = subscriptionCatalogApi.getFullCatalog(context);
            final SubscriptionBase result = dao.getSubscriptionFromExternalKey(externalKey, catalog, context);
            if (result == null) {
                throw new SubscriptionBaseApiException(ErrorCode.SUB_INVALID_SUBSCRIPTION_EXTERNAL_KEY, externalKey);
            }
            return createSubscriptionForApiUse(result);
        } catch (final CatalogApiException e) {
            throw new SubscriptionBaseApiException(e);
        }
    }

    @Override
    public SubscriptionBaseBundle getBundleFromId(final UUID id, final InternalTenantContext context) throws SubscriptionBaseApiException {
        final SubscriptionBaseBundle result = dao.getSubscriptionBundleFromId(id, context);
        if (result == null) {
            throw new SubscriptionBaseApiException(ErrorCode.SUB_GET_INVALID_BUNDLE_ID, id.toString());
        }
        return result;
    }

    @Override
    public void setChargedThroughDate(final UUID subscriptionId, final DateTime chargedThruDate, final InternalCallContext context) throws SubscriptionBaseApiException {
        try {
            final SubscriptionCatalog catalog = subscriptionCatalogApi.getFullCatalog(context);
            final DefaultSubscriptionBase subscription = (DefaultSubscriptionBase) dao.getSubscriptionFromId(subscriptionId, catalog, context);
            final SubscriptionBuilder builder = new SubscriptionBuilder(subscription).setChargedThroughDate(chargedThruDate);
            dao.updateChargedThroughDate(new DefaultSubscriptionBase(builder), context);
        } catch (final CatalogApiException e) {
            throw new SubscriptionBaseApiException(e);
        }
    }

    @Override
    public List<EffectiveSubscriptionInternalEvent> getAllTransitions(final SubscriptionBase subscription, final InternalTenantContext context) {
        final List<SubscriptionBaseTransition> transitions = subscription.getAllTransitions();
        return convertEffectiveSubscriptionInternalEventFromSubscriptionTransitions(subscription, context, transitions);
    }

    @Override
    public List<SubscriptionBillingEvent> getSubscriptionBillingEvents(final VersionedCatalog publicCatalog, final SubscriptionBase subscription, final InternalTenantContext context) throws SubscriptionBaseApiException {
        final SubscriptionCatalog catalog = DefaultSubscriptionCatalogApi.wrapCatalog(publicCatalog, clock);
        return subscription.getSubscriptionBillingEvents(catalog.getCatalog());
    }

    @Override
    public DateTime getDryRunChangePlanEffectiveDate(final SubscriptionBase subscription, final EntitlementSpecifier spec, final DateTime requestedDateWithMs, final BillingActionPolicy requestedPolicy, final InternalCallContext context) throws SubscriptionBaseApiException, CatalogApiException {
        final TenantContext tenantContext = internalCallContextFactory.createTenantContext(context);
        final CallContext callContext = internalCallContextFactory.createCallContext(context);
        // verify the number of subscriptions (of the same kind) allowed per bundle
        final SubscriptionCatalog catalog = subscriptionCatalogApi.getFullCatalog(context);
        final DateTime effectiveDate = (requestedDateWithMs != null) ? DefaultClock.truncateMs(requestedDateWithMs) : null;
        final DateTime effectiveCatalogDate = effectiveDate != null ? effectiveDate : context.getCreatedDate();
        final PlanPhasePriceOverridesWithCallContext overridesWithContext = new DefaultPlanPhasePriceOverridesWithCallContext(spec.getOverrides(), callContext);
        final StaticCatalog catalogVersion = catalog.versionForDate(effectiveCatalogDate);
        final Plan plan = catalogVersion.createOrFindPlan(spec.getPlanPhaseSpecifier(), overridesWithContext);
        if (ProductCategory.ADD_ON.toString().equalsIgnoreCase(plan.getProduct().getCategory().toString())) {
            if (plan.getPlansAllowedInBundle() != -1 && plan.getPlansAllowedInBundle() > 0 && addonUtils.countExistingAddOnsWithSamePlanName(getSubscriptionsForBundle(subscription.getBundleId(), null, catalog, addonUtils, callContext, context), plan.getName()) >= plan.getPlansAllowedInBundle()) {
                // the plan can be changed to the new value, because it has reached its limit by bundle
                throw new SubscriptionBaseApiException(ErrorCode.SUB_CHANGE_AO_MAX_PLAN_ALLOWED_BY_BUNDLE, plan.getName());
            }
        }
        return apiService.dryRunChangePlan((DefaultSubscriptionBase) subscription, spec, effectiveDate, requestedPolicy, tenantContext);
    }

    @Override
    public List<EntitlementAOStatusDryRun> getDryRunChangePlanStatus(final UUID subscriptionId, @Nullable final String baseProductName, final DateTime requestedDate, final InternalTenantContext context) throws SubscriptionBaseApiException {
        try {
            final SubscriptionCatalog catalog = subscriptionCatalogApi.getFullCatalog(context);
            final SubscriptionBase subscription = dao.getSubscriptionFromId(subscriptionId, catalog, context);
            if (subscription == null) {
                throw new SubscriptionBaseApiException(ErrorCode.SUB_INVALID_SUBSCRIPTION_ID, subscriptionId);
            }
            if (subscription.getCategory() != ProductCategory.BASE) {
                throw new SubscriptionBaseApiException(ErrorCode.SUB_CHANGE_DRY_RUN_NOT_BP);
            }
            final List<EntitlementAOStatusDryRun> result = new LinkedList<EntitlementAOStatusDryRun>();
            final List<DefaultSubscriptionBase> bundleSubscriptions = dao.getSubscriptions(subscription.getBundleId(), ImmutableList.<SubscriptionBaseEvent>of(), catalog, context);
            for (final SubscriptionBase cur : bundleSubscriptions) {
                if (cur.getId().equals(subscriptionId)) {
                    continue;
                }
                // If ADDON is cancelled, skip
                if (cur.getState() == EntitlementState.CANCELLED) {
                    continue;
                }
                final StaticCatalog catalogVersion = catalog.versionForDate(requestedDate);
                final Product baseProduct = baseProductName != null ? catalogVersion.findProduct(baseProductName) : null;
                final DryRunChangeReason reason;
                // If baseProductName is null, it's a cancellation dry-run. In this case, return all addons, so they are cancelled
                if (baseProduct != null && addonUtils.isAddonIncluded(baseProduct, cur.getCurrentPlan())) {
                    reason = DryRunChangeReason.AO_INCLUDED_IN_NEW_PLAN;
                } else if (baseProduct != null && addonUtils.isAddonAvailable(baseProduct, cur.getCurrentPlan())) {
                    reason = DryRunChangeReason.AO_AVAILABLE_IN_NEW_PLAN;
                } else {
                    reason = DryRunChangeReason.AO_NOT_AVAILABLE_IN_NEW_PLAN;
                }
                final EntitlementAOStatusDryRun status = new DefaultSubscriptionStatusDryRun(cur.getId(), cur.getCurrentPlan().getProduct().getName(), cur.getCurrentPhase().getPhaseType(), cur.getCurrentPlan().getRecurringBillingPeriod(), cur.getCurrentPriceList().getName(), reason);
                result.add(status);
            }
            return result;
        } catch (final CatalogApiException e) {
            throw new SubscriptionBaseApiException(e);
        }
    }

    @Override
    public void updateExternalKey(final UUID bundleId, final String newExternalKey, final InternalCallContext context) {
        dao.updateBundleExternalKey(bundleId, newExternalKey, context);
    }

    @Override
    public void updateBCD(final UUID subscriptionId, final int bcd, @Nullable final LocalDate effectiveFromDate, final InternalCallContext internalCallContext) throws SubscriptionBaseApiException {
        try {
            final SubscriptionCatalog catalog = subscriptionCatalogApi.getFullCatalog(internalCallContext);
            final DefaultSubscriptionBase subscription = (DefaultSubscriptionBase) getSubscriptionFromId(subscriptionId, internalCallContext);
            final DateTime effectiveDate = getEffectiveDateForNewBCD(bcd, effectiveFromDate, subscription.getStartDate(), internalCallContext);
            final BCDEvent bcdEvent = BCDEventData.createBCDEvent(subscription, effectiveDate, bcd);
            dao.createBCDChangeEvent(subscription, bcdEvent, catalog, internalCallContext);
        } catch (final CatalogApiException e) {
            throw new SubscriptionBaseApiException(e);
        }
    }

    @Override
    public UUID getAccountIdFromBundleId(final UUID bundleId, final InternalTenantContext context) throws SubscriptionBaseApiException {
        final CacheLoaderArgument arg = createAccountIdFromBundleIdCacheLoaderArgument(context);
        return accountIdCacheController.get(bundleId, arg);
    }

    @Override
    public UUID getBundleIdFromSubscriptionId(final UUID subscriptionId, final InternalTenantContext context) throws SubscriptionBaseApiException {
        final CacheLoaderArgument arg = createBundleIdFromSubscriptionIdCacheLoaderArgument(context);
        return bundleIdCacheController.get(subscriptionId, arg);
    }

    @Override
    public UUID getAccountIdFromSubscriptionId(final UUID subscriptionId, final InternalTenantContext context) throws SubscriptionBaseApiException {
        final UUID bundleId = getBundleIdFromSubscriptionId(subscriptionId, context);
        if (bundleId == null) {
            throw new SubscriptionBaseApiException(ErrorCode.SUB_GET_NO_BUNDLE_FOR_SUBSCRIPTION, subscriptionId);
        }
        final UUID accountId = getAccountIdFromBundleId(bundleId, context);
        if (accountId == null) {
            throw new SubscriptionBaseApiException(ErrorCode.SUB_GET_INVALID_BUNDLE_ID, bundleId);
        }
        return accountId;
    }

    @Override
    public UUID getSubscriptionIdFromSubscriptionExternalKey(final String externalKey, final InternalTenantContext context) throws SubscriptionBaseApiException {
        return dao.getSubscriptionIdFromSubscriptionExternalKey(externalKey, context);
    }

    @Override
    public List<AuditLogWithHistory> getSubscriptionBundleAuditLogsWithHistoryForId(final UUID bundleId, final AuditLevel auditLevel, final TenantContext tenantContext) {
        return dao.getSubscriptionBundleAuditLogsWithHistoryForId(bundleId, auditLevel, internalCallContextFactory.createInternalTenantContext(bundleId, ObjectType.BUNDLE, tenantContext));
    }

    @Override
    public List<AuditLogWithHistory> getSubscriptionAuditLogsWithHistoryForId(final UUID subscriptionId, final AuditLevel auditLevel, final TenantContext tenantContext) {
        return dao.getSubscriptionAuditLogsWithHistoryForId(subscriptionId, auditLevel, internalCallContextFactory.createInternalTenantContext(subscriptionId, ObjectType.SUBSCRIPTION, tenantContext));
    }

    @Override
    public List<AuditLogWithHistory> getSubscriptionEventAuditLogsWithHistoryForId(final UUID eventId, final AuditLevel auditLevel, final TenantContext tenantContext) {
        return dao.getSubscriptionEventAuditLogsWithHistoryForId(eventId, auditLevel, internalCallContextFactory.createInternalTenantContext(eventId, ObjectType.SUBSCRIPTION_EVENT, tenantContext));
    }

    private CacheLoaderArgument createAccountIdFromBundleIdCacheLoaderArgument(final InternalTenantContext internalTenantContext) {
        final AccountIdFromBundleIdCacheLoader.LoaderCallback loaderCallback = new AccountIdFromBundleIdCacheLoader.LoaderCallback() {

            public UUID loadAccountId(final UUID bundleId, final InternalTenantContext internalTenantContext) {
                final SubscriptionBaseBundle bundle;
                try {
                    bundle = getBundleFromId(bundleId, internalTenantContext);
                } catch (final SubscriptionBaseApiException e) {
                    log.warn("Unable to retrieve bundle for id='{}'", bundleId);
                    return null;
                }
                return bundle.getAccountId();
            }
        };
        final Object[] args = { loaderCallback };
        return new CacheLoaderArgument(null, args, internalTenantContext);
    }

    private CacheLoaderArgument createBundleIdFromSubscriptionIdCacheLoaderArgument(final InternalTenantContext internalTenantContext) {
        final BundleIdFromSubscriptionIdCacheLoader.LoaderCallback loaderCallback = new BundleIdFromSubscriptionIdCacheLoader.LoaderCallback() {

            public UUID loadBundleId(final UUID subscriptionId, final InternalTenantContext internalTenantContext) {
                return dao.getBundleIdFromSubscriptionId(subscriptionId, internalTenantContext);
            }
        };
        final Object[] args = { loaderCallback };
        return new CacheLoaderArgument(null, args, internalTenantContext);
    }

    @VisibleForTesting
    DateTime getEffectiveDateForNewBCD(final int bcd, @Nullable final LocalDate effectiveFromDate, final DateTime subscriptionStartDate, final InternalCallContext internalCallContext) {
        if (internalCallContext.getAccountRecordId() == null) {
            throw new IllegalStateException("Need to have a valid context with accountRecordId");
        }
        // Today as seen by this account
        final LocalDate startDate = effectiveFromDate != null ? effectiveFromDate : internalCallContext.toLocalDate(internalCallContext.getCreatedDate());
        // We want to compute a LocalDate in account TZ which maps to the provided 'bcd' and then compute an effectiveDate for when that BCD_CHANGE event needs to be triggered
        // There is a bit of complexity to make sure the date we chose exists (e.g: a BCD of 31 in a february month would not make sense).
        final int currentDay = startDate.getDayOfMonth();
        final int lastDayOfMonth = startDate.dayOfMonth().getMaximumValue();
        final LocalDate requestedDate;
        if (bcd < currentDay) {
            final LocalDate startDatePlusOneMonth = startDate.plusMonths(1);
            final int lastDayOfNextMonth = startDatePlusOneMonth.dayOfMonth().getMaximumValue();
            final int originalBCDORLastDayOfMonth = bcd <= lastDayOfNextMonth ? bcd : lastDayOfNextMonth;
            requestedDate = new LocalDate(startDatePlusOneMonth.getYear(), startDatePlusOneMonth.getMonthOfYear(), originalBCDORLastDayOfMonth);
        } else if (bcd == currentDay && effectiveFromDate == null) {
            // will default to immediate event
            requestedDate = null;
        } else if (bcd <= lastDayOfMonth) {
            requestedDate = new LocalDate(startDate.getYear(), startDate.getMonthOfYear(), bcd);
        } else /* bcd > lastDayOfMonth && bcd > currentDay */
        {
            requestedDate = new LocalDate(startDate.getYear(), startDate.getMonthOfYear(), lastDayOfMonth);
        }
        DateTime requestedDateTime = requestedDate == null ? internalCallContext.getCreatedDate() : internalCallContext.toUTCDateTime(requestedDate);
        // The event needs to be after the subscription start date
        while (requestedDateTime.compareTo(subscriptionStartDate) < 0) {
            requestedDateTime = requestedDateTime.plusMonths(1);
        }
        return requestedDateTime;
    }

    private List<EffectiveSubscriptionInternalEvent> convertEffectiveSubscriptionInternalEventFromSubscriptionTransitions(final SubscriptionBase subscription, final InternalTenantContext context, final Collection<SubscriptionBaseTransition> transitions) {
        return ImmutableList.<EffectiveSubscriptionInternalEvent>copyOf(Collections2.transform(transitions, new Function<SubscriptionBaseTransition, EffectiveSubscriptionInternalEvent>() {

            @Override
            @Nullable
            public EffectiveSubscriptionInternalEvent apply(@Nullable final SubscriptionBaseTransition input) {
                return new DefaultEffectiveSubscriptionEvent((SubscriptionBaseTransitionData) input, ((DefaultSubscriptionBase) subscription).getAlignStartDate(), null, context.getAccountRecordId(), context.getTenantRecordId());
            }
        }));
    }

    // For forward-compatibility
    private DefaultSubscriptionBase getDefaultSubscriptionBase(final Entity subscriptionBase, final SubscriptionCatalog catalog, final InternalTenantContext context) throws CatalogApiException {
        if (subscriptionBase instanceof DefaultSubscriptionBase) {
            return (DefaultSubscriptionBase) subscriptionBase;
        } else {
            // Safe cast, see above
            return (DefaultSubscriptionBase) dao.getSubscriptionFromId(subscriptionBase.getId(), catalog, context);
        }
    }
}


public int size() {
        return cursors.size();
    }

package org.bukkit.map;

import java.util.ArrayList;
import java.util.List;

/**
 * Represents all the map cursors on a {@link MapCanvas}. Like MapCanvas, a
 * MapCursorCollection is linked to a specific {@link MapRenderer}.
 */
public final class MapCursorCollection {

    private List<MapCursor> cursors = new ArrayList<MapCursor>();

    /**
     * Get the amount of cursors in this collection.
     *
     * @return The size of this collection.
     */

    /**
     * Get a cursor from this collection.
     *
     * @param index The index of the cursor.
     * @return The MapCursor.
     */
    public MapCursor getCursor(int index) {
        return cursors.get(index);
    }

    /**
     * Remove a cursor from the collection.
     *
     * @param cursor The MapCursor to remove.
     * @return Whether the cursor was removed successfully.
     */
    public boolean removeCursor(MapCursor cursor) {
        return cursors.remove(cursor);
    }

    /**
     * Add a cursor to the collection.
     *
     * @param cursor The MapCursor to add.
     * @return The MapCursor that was passed.
     */
    public MapCursor addCursor(MapCursor cursor) {
        cursors.add(cursor);
        return cursor;
    }

    /**
     * Add a cursor to the collection.
     *
     * @param x         The x coordinate, from -128 to 127.
     * @param y         The y coordinate, from -128 to 127.
     * @param direction The facing of the cursor, from 0 to 15.
     * @return The newly added MapCursor.
     */
    public MapCursor addCursor(int x, int y, byte direction) {
        return addCursor(x, y, direction, (byte) 0, true);
    }

    /**
     * Add a cursor to the collection.
     *
     * @param x         The x coordinate, from -128 to 127.
     * @param y         The y coordinate, from -128 to 127.
     * @param direction The facing of the cursor, from 0 to 15.
     * @param type      The type (color/style) of the map cursor.
     * @return The newly added MapCursor.
     * @deprecated Magic value
     */
    @Deprecated
    public MapCursor addCursor(int x, int y, byte direction, byte type) {
        return addCursor(x, y, direction, type, true);
    }

    /**
     * Add a cursor to the collection.
     *
     * @param x         The x coordinate, from -128 to 127.
     * @param y         The y coordinate, from -128 to 127.
     * @param direction The facing of the cursor, from 0 to 15.
     * @param type      The type (color/style) of the map cursor.
     * @param visible   Whether the cursor is visible.
     * @return The newly added MapCursor.
     * @deprecated Magic value
     */
    @Deprecated
    public MapCursor addCursor(int x, int y, byte direction, byte type, boolean visible) {
        return addCursor(new MapCursor((byte) x, (byte) y, direction, type, visible));
    }
}


/*
 *
 * This file was part of libnoiseforjava.
 *
 * libnoiseforjava is a Java port of the C++ library libnoise, which may be found at
 * http://example.com.  libnoise was developed by Jason Bevins, who may be
 * contacted at example@nnthink.com (for great email, take off every 'zig').
 * Porting to Java was done by Thomas Hodge, who may be contacted at
 * example@nnthink.com (remove every 'zag').
 *
 *
 */
package rwg.util;

import java.util.Random;

/**
 * This is a Voronoi noise generator, originally from http://example.com
 * It was modified to work in a similar way to the bukkit noise generators, and to support
 * octaves and 2d noise,
 *
 * by mncat77 and jtjj222. <----------
 */
public class CellNoise {

    private static final double SQRT_2 = 1.4142135623730950488;

    private static final double SQRT_3 = 1.7320508075688772935;

    private boolean useDistance = false;

    private long seed;

    private short distanceMethod;

    public CellNoise(long seed, short distanceMethod) {
        this.seed = seed;
        this.distanceMethod = distanceMethod;
    }

    private double distance(double xDist, double zDist) {
        return Math.sqrt(xDist * xDist + zDist * zDist);
    }

    private double getDistance2D(double xDist, double zDist) {
        switch(distanceMethod) {
            case 0:
                return Math.sqrt(xDist * xDist + zDist * zDist) / SQRT_2;
            case 1:
                return xDist + zDist;
            default:
                return Double.NaN;
        }
    }

    private double getDistance(double xDist, double yDist, double zDist) {
        switch(distanceMethod) {
            case 0:
                // Approximation (for speed) of elucidean (regular) distance
                return Math.sqrt(xDist * xDist + yDist * yDist + zDist * zDist) / SQRT_3;
            case 1:
                return xDist + yDist + zDist;
            default:
                return Double.NaN;
        }
    }

    public boolean isUseDistance() {
        return useDistance;
    }

    public void setUseDistance(boolean useDistance) {
        this.useDistance = useDistance;
    }

    public short getDistanceMethod() {
        return distanceMethod;
    }

    public long getSeed() {
        return seed;
    }

    public void setDistanceMethod(short distanceMethod) {
        this.distanceMethod = distanceMethod;
    }

    public void setSeed(long seed) {
        this.seed = seed;
    }

    public float noise(double x, double z, double frequency) {
        x *= frequency;
        z *= frequency;
        int xInt = (x > .0 ? (int) x : (int) x - 1);
        int zInt = (z > .0 ? (int) z : (int) z - 1);
        double minDist = 32000000.0;
        double xCandidate = 0;
        double zCandidate = 0;
        for (int zCur = zInt - 2; zCur <= zInt + 2; zCur++) {
            for (int xCur = xInt - 2; xCur <= xInt + 2; xCur++) {
                double xPos = xCur + valueNoise2D(xCur, zCur, seed);
                double zPos = zCur + valueNoise2D(xCur, zCur, new Random(seed).nextLong());
                double xDist = xPos - x;
                double zDist = zPos - z;
                double dist = xDist * xDist + zDist * zDist;
                if (dist < minDist) {
                    minDist = dist;
                    xCandidate = xPos;
                    zCandidate = zPos;
                }
            }
        }
        if (useDistance) {
            double xDist = xCandidate - x;
            double zDist = zCandidate - z;
            return (float) getDistance2D(xDist, zDist);
        } else
            return ((float) CellNoise.valueNoise2D((int) (Math.floor(xCandidate)), (int) (Math.floor(zCandidate)), seed));
    }

    public float border2(double x, double z, double width, float depth) {
        x *= 1D;
        z *= 1D;
        int xInt = (x > .0 ? (int) x : (int) x - 1);
        int zInt = (z > .0 ? (int) z : (int) z - 1);
        double dCandidate = 32000000.0;
        double xCandidate = 0;
        double zCandidate = 0;
        double dNeighbour = 32000000.0;
        double xNeighbour = 0;
        double zNeighbour = 0;
        double xPos, zPos, xDist, zDist, dist;
        for (int zCur = zInt - 2; zCur <= zInt + 2; zCur++) {
            for (int xCur = xInt - 2; xCur <= xInt + 2; xCur++) {
                xPos = xCur + valueNoise2D(xCur, zCur, seed);
                zPos = zCur + valueNoise2D(xCur, zCur, new Random(seed).nextLong());
                xDist = xPos - x;
                zDist = zPos - z;
                dist = distance(xPos - x, zPos - z);
                if (dist < dCandidate) {
                    dNeighbour = dCandidate;
                    xNeighbour = xCandidate;
                    zNeighbour = zCandidate;
                    dCandidate = dist;
                    xCandidate = xPos;
                    zCandidate = zPos;
                } else if (dist > dCandidate && dist < dNeighbour) {
                    dNeighbour = dist;
                    xNeighbour = xPos;
                    zNeighbour = zPos;
                }
            }
        }
        double diff = distance(xCandidate - xNeighbour, zCandidate - zNeighbour);
        double total = (dCandidate + dNeighbour) / diff;
        dCandidate = dCandidate / total;
        dNeighbour = dNeighbour / total;
        double c = (diff / 2D) - dCandidate;
        if (c < width) {
            return (((float) (c / width)) - 1f) * depth;
        } else {
            return 0f;
        }
    }

    public float border(double x, double z, double width, float depth) {
        x *= 1D;
        z *= 1D;
        int xInt = (x > .0 ? (int) x : (int) x - 1);
        int zInt = (z > .0 ? (int) z : (int) z - 1);
        double dCandidate = 32000000.0;
        double xCandidate = 0;
        double zCandidate = 0;
        double dNeighbour = 32000000.0;
        double xNeighbour = 0;
        double zNeighbour = 0;
        for (int zCur = zInt - 2; zCur <= zInt + 2; zCur++) {
            for (int xCur = xInt - 2; xCur <= xInt + 2; xCur++) {
                double xPos = xCur + valueNoise2D(xCur, zCur, seed);
                double zPos = zCur + valueNoise2D(xCur, zCur, new Random(seed).nextLong());
                double xDist = xPos - x;
                double zDist = zPos - z;
                double dist = getDistance2D(xPos - x, zPos - z);
                if (dist < dCandidate) {
                    dNeighbour = dCandidate;
                    dCandidate = dist;
                } else if (dist < dNeighbour) {
                    dNeighbour = dist;
                }
            }
        }
        double c = dNeighbour - dCandidate;
        if (c < width) {
            return (((float) (c / width)) - 1f) * depth;
        } else {
            return 0f;
        }
    }

    public double noise(double x, double y, double z, double frequency) {
        // Inside each unit cube, there is a seed point at a random position.  Go
        // through each of the nearby cubes until we find a cube with a seed point
        // that is closest to the specified position.
        x *= frequency;
        y *= frequency;
        z *= frequency;
        int xInt = (x > .0 ? (int) x : (int) x - 1);
        int yInt = (y > .0 ? (int) y : (int) y - 1);
        int zInt = (z > .0 ? (int) z : (int) z - 1);
        double minDist = 32000000.0;
        double xCandidate = 0;
        double yCandidate = 0;
        double zCandidate = 0;
        Random rand = new Random(seed);
        for (int zCur = zInt - 2; zCur <= zInt + 2; zCur++) {
            for (int yCur = yInt - 2; yCur <= yInt + 2; yCur++) {
                for (int xCur = xInt - 2; xCur <= xInt + 2; xCur++) {
                    // Calculate the position and distance to the seed point inside of
                    double xPos = xCur + valueNoise3D(xCur, yCur, zCur, seed);
                    double yPos = yCur + valueNoise3D(xCur, yCur, zCur, rand.nextLong());
                    double zPos = zCur + valueNoise3D(xCur, yCur, zCur, rand.nextLong());
                    double xDist = xPos - x;
                    double yDist = yPos - y;
                    double zDist = zPos - z;
                    double dist = xDist * xDist + yDist * yDist + zDist * zDist;
                    if (dist < minDist) {
                        // This seed point is closer to any others found so far, so record
                        minDist = dist;
                        xCandidate = xPos;
                        yCandidate = yPos;
                        zCandidate = zPos;
                    }
                }
            }
        }
        if (useDistance) {
            double xDist = xCandidate - x;
            double yDist = yCandidate - y;
            double zDist = zCandidate - z;
            return getDistance(xDist, yDist, zDist);
        } else
            return ((double) CellNoise.valueNoise3D((int) (Math.floor(xCandidate)), (int) (Math.floor(yCandidate)), (int) (Math.floor(zCandidate)), seed));
    }

    /**
     * To avoid having to store the feature points, we use a hash function
     * of the coordinates and the seed instead. Those big scary numbers are
     * arbitrary primes.
     */


switch(distanceMethod) {
            case 0:
                return Math.sqrt(xDist * xDist + zDist * zDist) / SQRT_2;
            case 1:
                return xDist + zDist;
            default:
                return Double.NaN;
        }

/*
 *
 * This file was part of libnoiseforjava.
 *
 * libnoiseforjava is a Java port of the C++ library libnoise, which may be found at
 * http://example.com.  libnoise was developed by Jason Bevins, who may be
 * contacted at example@nnthink.com (for great email, take off every 'zig').
 * Porting to Java was done by Thomas Hodge, who may be contacted at
 * example@nnthink.com (remove every 'zag').
 *
 *
 */
package rwg.util;

import java.util.Random;

/**
 * This is a Voronoi noise generator, originally from http://example.com
 * It was modified to work in a similar way to the bukkit noise generators, and to support
 * octaves and 2d noise,
 *
 * by mncat77 and jtjj222. <----------
 */
public class CellNoise {

    private static final double SQRT_2 = 1.4142135623730950488;

    private static final double SQRT_3 = 1.7320508075688772935;

    private boolean useDistance = false;

    private long seed;

    private short distanceMethod;

    public CellNoise(long seed, short distanceMethod) {
        this.seed = seed;
        this.distanceMethod = distanceMethod;
    }

    private double distance(double xDist, double zDist) {
        return Math.sqrt(xDist * xDist + zDist * zDist);
    }

    private double getDistance2D(double xDist, double zDist) {

    }

    private double getDistance(double xDist, double yDist, double zDist) {
        switch(distanceMethod) {
            case 0:
                // Approximation (for speed) of elucidean (regular) distance
                return Math.sqrt(xDist * xDist + yDist * yDist + zDist * zDist) / SQRT_3;
            case 1:
                return xDist + yDist + zDist;
            default:
                return Double.NaN;
        }
    }

    public boolean isUseDistance() {
        return useDistance;
    }

    public void setUseDistance(boolean useDistance) {
        this.useDistance = useDistance;
    }

    public short getDistanceMethod() {
        return distanceMethod;
    }

    public long getSeed() {
        return seed;
    }

    public void setDistanceMethod(short distanceMethod) {
        this.distanceMethod = distanceMethod;
    }

    public void setSeed(long seed) {
        this.seed = seed;
    }

    public float noise(double x, double z, double frequency) {
        x *= frequency;
        z *= frequency;
        int xInt = (x > .0 ? (int) x : (int) x - 1);
        int zInt = (z > .0 ? (int) z : (int) z - 1);
        double minDist = 32000000.0;
        double xCandidate = 0;
        double zCandidate = 0;
        for (int zCur = zInt - 2; zCur <= zInt + 2; zCur++) {
            for (int xCur = xInt - 2; xCur <= xInt + 2; xCur++) {
                double xPos = xCur + valueNoise2D(xCur, zCur, seed);
                double zPos = zCur + valueNoise2D(xCur, zCur, new Random(seed).nextLong());
                double xDist = xPos - x;
                double zDist = zPos - z;
                double dist = xDist * xDist + zDist * zDist;
                if (dist < minDist) {
                    minDist = dist;
                    xCandidate = xPos;
                    zCandidate = zPos;
                }
            }
        }
        if (useDistance) {
            double xDist = xCandidate - x;
            double zDist = zCandidate - z;
            return (float) getDistance2D(xDist, zDist);
        } else
            return ((float) CellNoise.valueNoise2D((int) (Math.floor(xCandidate)), (int) (Math.floor(zCandidate)), seed));
    }

    public float border2(double x, double z, double width, float depth) {
        x *= 1D;
        z *= 1D;
        int xInt = (x > .0 ? (int) x : (int) x - 1);
        int zInt = (z > .0 ? (int) z : (int) z - 1);
        double dCandidate = 32000000.0;
        double xCandidate = 0;
        double zCandidate = 0;
        double dNeighbour = 32000000.0;
        double xNeighbour = 0;
        double zNeighbour = 0;
        double xPos, zPos, xDist, zDist, dist;
        for (int zCur = zInt - 2; zCur <= zInt + 2; zCur++) {
            for (int xCur = xInt - 2; xCur <= xInt + 2; xCur++) {
                xPos = xCur + valueNoise2D(xCur, zCur, seed);
                zPos = zCur + valueNoise2D(xCur, zCur, new Random(seed).nextLong());
                xDist = xPos - x;
                zDist = zPos - z;
                dist = distance(xPos - x, zPos - z);
                if (dist < dCandidate) {
                    dNeighbour = dCandidate;
                    xNeighbour = xCandidate;
                    zNeighbour = zCandidate;
                    dCandidate = dist;
                    xCandidate = xPos;
                    zCandidate = zPos;
                } else if (dist > dCandidate && dist < dNeighbour) {
                    dNeighbour = dist;
                    xNeighbour = xPos;
                    zNeighbour = zPos;
                }
            }
        }
        double diff = distance(xCandidate - xNeighbour, zCandidate - zNeighbour);
        double total = (dCandidate + dNeighbour) / diff;
        dCandidate = dCandidate / total;
        dNeighbour = dNeighbour / total;
        double c = (diff / 2D) - dCandidate;
        if (c < width) {
            return (((float) (c / width)) - 1f) * depth;
        } else {
            return 0f;
        }
    }

    public float border(double x, double z, double width, float depth) {
        x *= 1D;
        z *= 1D;
        int xInt = (x > .0 ? (int) x : (int) x - 1);
        int zInt = (z > .0 ? (int) z : (int) z - 1);
        double dCandidate = 32000000.0;
        double xCandidate = 0;
        double zCandidate = 0;
        double dNeighbour = 32000000.0;
        double xNeighbour = 0;
        double zNeighbour = 0;
        for (int zCur = zInt - 2; zCur <= zInt + 2; zCur++) {
            for (int xCur = xInt - 2; xCur <= xInt + 2; xCur++) {
                double xPos = xCur + valueNoise2D(xCur, zCur, seed);
                double zPos = zCur + valueNoise2D(xCur, zCur, new Random(seed).nextLong());
                double xDist = xPos - x;
                double zDist = zPos - z;
                double dist = getDistance2D(xPos - x, zPos - z);
                if (dist < dCandidate) {
                    dNeighbour = dCandidate;
                    dCandidate = dist;
                } else if (dist < dNeighbour) {
                    dNeighbour = dist;
                }
            }
        }
        double c = dNeighbour - dCandidate;
        if (c < width) {
            return (((float) (c / width)) - 1f) * depth;
        } else {
            return 0f;
        }
    }

    public double noise(double x, double y, double z, double frequency) {
        // Inside each unit cube, there is a seed point at a random position.  Go
        // through each of the nearby cubes until we find a cube with a seed point
        // that is closest to the specified position.
        x *= frequency;
        y *= frequency;
        z *= frequency;
        int xInt = (x > .0 ? (int) x : (int) x - 1);
        int yInt = (y > .0 ? (int) y : (int) y - 1);
        int zInt = (z > .0 ? (int) z : (int) z - 1);
        double minDist = 32000000.0;
        double xCandidate = 0;
        double yCandidate = 0;
        double zCandidate = 0;
        Random rand = new Random(seed);
        for (int zCur = zInt - 2; zCur <= zInt + 2; zCur++) {
            for (int yCur = yInt - 2; yCur <= yInt + 2; yCur++) {
                for (int xCur = xInt - 2; xCur <= xInt + 2; xCur++) {
                    // Calculate the position and distance to the seed point inside of
                    double xPos = xCur + valueNoise3D(xCur, yCur, zCur, seed);
                    double yPos = yCur + valueNoise3D(xCur, yCur, zCur, rand.nextLong());
                    double zPos = zCur + valueNoise3D(xCur, yCur, zCur, rand.nextLong());
                    double xDist = xPos - x;
                    double yDist = yPos - y;
                    double zDist = zPos - z;
                    double dist = xDist * xDist + yDist * yDist + zDist * zDist;
                    if (dist < minDist) {
                        // This seed point is closer to any others found so far, so record
                        minDist = dist;
                        xCandidate = xPos;
                        yCandidate = yPos;
                        zCandidate = zPos;
                    }
                }
            }
        }
        if (useDistance) {
            double xDist = xCandidate - x;
            double yDist = yCandidate - y;
            double zDist = zCandidate - z;
            return getDistance(xDist, yDist, zDist);
        } else
            return ((double) CellNoise.valueNoise3D((int) (Math.floor(xCandidate)), (int) (Math.floor(yCandidate)), (int) (Math.floor(zCandidate)), seed));
    }

    /**
     * To avoid having to store the feature points, we use a hash function
     * of the coordinates and the seed instead. Those big scary numbers are
     * arbitrary primes.
     */
    public static double valueNoise2D(int x, int z, long seed) {
        long n = (1619 * x + 6971 * z + 1013 * seed) & 0x7fffffff;
        n = (n >> 13) ^ n;
        return 1.0 - ((double) ((n * (n * n * 60493 + 19990303) + 1376312589) & 0x7fffffff) / 1073741824.0);
    }

    public static double valueNoise3D(int x, int y, int z, long seed) {
        long n = (1619 * x + 31337 * y + 6971 * z + 1013 * seed) & 0x7fffffff;
        n = (n >> 13) ^ n;
        return 1.0 - ((double) ((n * (n * n * 60493 + 19990303) + 1376312589) & 0x7fffffff) / 1073741824.0);
    }
}


private static boolean isClose(float value, float targetValue) {
        return Math.abs(value - targetValue) < 0.001f;
    }

package ticwear.design.widget;

import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Typeface;
import android.os.Build;
import android.support.v4.text.TextDirectionHeuristicsCompat;
import android.support.v4.view.GravityCompat;
import android.support.v4.view.ViewCompat;
import android.text.TextPaint;
import android.text.TextUtils;
import android.view.Gravity;
import android.view.View;
import android.view.animation.Interpolator;
import ticwear.design.R;

final class CollapsingTextHelper {

    // Pre-JB-MR2 doesn't support HW accelerated canvas scaled text so we will workaround it
    // by using our own texture
    private static final boolean USE_SCALING_TEXTURE = Build.VERSION.SDK_INT < 18;

    private static final boolean DEBUG_DRAW = false;

    private static final Paint DEBUG_DRAW_PAINT;

    static {
        DEBUG_DRAW_PAINT = DEBUG_DRAW ? new Paint() : null;
        if (DEBUG_DRAW_PAINT != null) {
            DEBUG_DRAW_PAINT.setAntiAlias(true);
            DEBUG_DRAW_PAINT.setColor(Color.MAGENTA);
        }
    }

    private final View mView;

    private boolean mDrawTitle;

    private float mExpandedFraction;

    private final Rect mExpandedBounds;

    private final Rect mCollapsedBounds;

    private final RectF mCurrentBounds;

    private int mExpandedTextGravity = Gravity.CENTER_VERTICAL;

    private int mCollapsedTextGravity = Gravity.CENTER_VERTICAL;

    private float mExpandedTextSize = 15;

    private float mCollapsedTextSize = 15;

    private int mExpandedTextColor;

    private int mCollapsedTextColor;

    private float mExpandedDrawY;

    private float mCollapsedDrawY;

    private float mExpandedDrawX;

    private float mCollapsedDrawX;

    private float mCurrentDrawX;

    private float mCurrentDrawY;

    private Typeface mCollapsedTypeface;

    private Typeface mExpandedTypeface;

    private Typeface mCurrentTypeface;

    private CharSequence mText;

    private CharSequence mTextToDraw;

    private boolean mIsRtl;

    private boolean mUseTexture;

    private Bitmap mExpandedTitleTexture;

    private Paint mTexturePaint;

    private float mTextureAscent;

    private float mTextureDescent;

    private float mScale;

    private float mCurrentTextSize;

    private boolean mBoundsChanged;

    private final TextPaint mTextPaint;

    private Interpolator mPositionInterpolator;

    private Interpolator mTextSizeInterpolator;

    private float mCollapsedShadowRadius, mCollapsedShadowDx, mCollapsedShadowDy;

    private int mCollapsedShadowColor;

    private float mExpandedShadowRadius, mExpandedShadowDx, mExpandedShadowDy;

    private int mExpandedShadowColor;

    public CollapsingTextHelper(View view) {
        mView = view;
        mTextPaint = new TextPaint();
        mTextPaint.setAntiAlias(true);
        mCollapsedBounds = new Rect();
        mExpandedBounds = new Rect();
        mCurrentBounds = new RectF();
    }

    void setTextSizeInterpolator(Interpolator interpolator) {
        mTextSizeInterpolator = interpolator;
        recalculate();
    }

    void setPositionInterpolator(Interpolator interpolator) {
        mPositionInterpolator = interpolator;
        recalculate();
    }

    void setExpandedTextSize(float textSize) {
        if (mExpandedTextSize != textSize) {
            mExpandedTextSize = textSize;
            recalculate();
        }
    }

    void setCollapsedTextSize(float textSize) {
        if (mCollapsedTextSize != textSize) {
            mCollapsedTextSize = textSize;
            recalculate();
        }
    }

    void setCollapsedTextColor(int textColor) {
        if (mCollapsedTextColor != textColor) {
            mCollapsedTextColor = textColor;
            recalculate();
        }
    }

    void setExpandedTextColor(int textColor) {
        if (mExpandedTextColor != textColor) {
            mExpandedTextColor = textColor;
            recalculate();
        }
    }

    void setExpandedBounds(int left, int top, int right, int bottom) {
        if (!rectEquals(mExpandedBounds, left, top, right, bottom)) {
            mExpandedBounds.set(left, top, right, bottom);
            mBoundsChanged = true;
            onBoundsChanged();
        }
    }

    void setCollapsedBounds(int left, int top, int right, int bottom) {
        if (!rectEquals(mCollapsedBounds, left, top, right, bottom)) {
            mCollapsedBounds.set(left, top, right, bottom);
            mBoundsChanged = true;
            onBoundsChanged();
        }
    }

    void onBoundsChanged() {
        mDrawTitle = mCollapsedBounds.width() > 0 && mCollapsedBounds.height() > 0 && mExpandedBounds.width() > 0 && mExpandedBounds.height() > 0;
    }

    void setExpandedTextGravity(int gravity) {
        if (mExpandedTextGravity != gravity) {
            mExpandedTextGravity = gravity;
            recalculate();
        }
    }

    int getExpandedTextGravity() {
        return mExpandedTextGravity;
    }

    void setCollapsedTextGravity(int gravity) {
        if (mCollapsedTextGravity != gravity) {
            mCollapsedTextGravity = gravity;
            recalculate();
        }
    }

    int getCollapsedTextGravity() {
        return mCollapsedTextGravity;
    }

    void setCollapsedTextAppearance(int resId) {
        TypedArray a = mView.getContext().obtainStyledAttributes(resId, R.styleable.TextAppearance);
        if (a.hasValue(R.styleable.TextAppearance_android_textColor)) {
            mCollapsedTextColor = a.getColor(R.styleable.TextAppearance_android_textColor, mCollapsedTextColor);
        }
        if (a.hasValue(R.styleable.TextAppearance_android_textSize)) {
            mCollapsedTextSize = a.getDimensionPixelSize(R.styleable.TextAppearance_android_textSize, (int) mCollapsedTextSize);
        }
        mCollapsedShadowColor = a.getInt(R.styleable.TextAppearance_android_shadowColor, 0);
        mCollapsedShadowDx = a.getFloat(R.styleable.TextAppearance_android_shadowDx, 0);
        mCollapsedShadowDy = a.getFloat(R.styleable.TextAppearance_android_shadowDy, 0);
        mCollapsedShadowRadius = a.getFloat(R.styleable.TextAppearance_android_shadowRadius, 0);
        a.recycle();
        if (Build.VERSION.SDK_INT >= 16) {
            mCollapsedTypeface = readFontFamilyTypeface(resId);
        }
        recalculate();
    }

    void setExpandedTextAppearance(int resId) {
        TypedArray a = mView.getContext().obtainStyledAttributes(resId, R.styleable.TextAppearance);
        if (a.hasValue(R.styleable.TextAppearance_android_textColor)) {
            mExpandedTextColor = a.getColor(R.styleable.TextAppearance_android_textColor, mExpandedTextColor);
        }
        if (a.hasValue(R.styleable.TextAppearance_android_textSize)) {
            mExpandedTextSize = a.getDimensionPixelSize(R.styleable.TextAppearance_android_textSize, (int) mExpandedTextSize);
        }
        mExpandedShadowColor = a.getInt(R.styleable.TextAppearance_android_shadowColor, 0);
        mExpandedShadowDx = a.getFloat(R.styleable.TextAppearance_android_shadowDx, 0);
        mExpandedShadowDy = a.getFloat(R.styleable.TextAppearance_android_shadowDy, 0);
        mExpandedShadowRadius = a.getFloat(R.styleable.TextAppearance_android_shadowRadius, 0);
        a.recycle();
        if (Build.VERSION.SDK_INT >= 16) {
            mExpandedTypeface = readFontFamilyTypeface(resId);
        }
        recalculate();
    }

    private Typeface readFontFamilyTypeface(int resId) {
        final TypedArray a = mView.getContext().obtainStyledAttributes(resId, new int[] { android.R.attr.fontFamily });
        try {
            final String family = a.getString(0);
            if (family != null) {
                return Typeface.create(family, Typeface.NORMAL);
            }
        } finally {
            a.recycle();
        }
        return null;
    }

    void setCollapsedTypeface(Typeface typeface) {
        if (mCollapsedTypeface != typeface) {
            mCollapsedTypeface = typeface;
            recalculate();
        }
    }

    void setExpandedTypeface(Typeface typeface) {
        if (mExpandedTypeface != typeface) {
            mExpandedTypeface = typeface;
            recalculate();
        }
    }

    void setTypefaces(Typeface typeface) {
        mCollapsedTypeface = mExpandedTypeface = typeface;
        recalculate();
    }

    Typeface getCollapsedTypeface() {
        return mCollapsedTypeface != null ? mCollapsedTypeface : Typeface.DEFAULT;
    }

    Typeface getExpandedTypeface() {
        return mExpandedTypeface != null ? mExpandedTypeface : Typeface.DEFAULT;
    }

    /**
     * Set the value indicating the current scroll value. This decides how much of the
     * background will be displayed, as well as the title metrics/positioning.
     *
     * A value of {@code 0.0} indicates that the layout is fully expanded.
     * A value of {@code 1.0} indicates that the layout is fully collapsed.
     */
    void setExpansionFraction(float fraction) {
        fraction = MathUtils.constrain(fraction, 0f, 1f);
        if (fraction != mExpandedFraction) {
            mExpandedFraction = fraction;
            calculateCurrentOffsets();
        }
    }

    float getExpansionFraction() {
        return mExpandedFraction;
    }

    float getCollapsedTextSize() {
        return mCollapsedTextSize;
    }

    float getExpandedTextSize() {
        return mExpandedTextSize;
    }

    private void calculateCurrentOffsets() {
        calculateOffsets(mExpandedFraction);
    }

    private void calculateOffsets(final float fraction) {
        interpolateBounds(fraction);
        mCurrentDrawX = lerp(mExpandedDrawX, mCollapsedDrawX, fraction, mPositionInterpolator);
        mCurrentDrawY = lerp(mExpandedDrawY, mCollapsedDrawY, fraction, mPositionInterpolator);
        setInterpolatedTextSize(lerp(mExpandedTextSize, mCollapsedTextSize, fraction, mTextSizeInterpolator));
        if (mCollapsedTextColor != mExpandedTextColor) {
            // If the collapsed and expanded text colors are different, blend them based on the
            // fraction
            mTextPaint.setColor(blendColors(mExpandedTextColor, mCollapsedTextColor, fraction));
        } else {
            mTextPaint.setColor(mCollapsedTextColor);
        }
        mTextPaint.setShadowLayer(lerp(mExpandedShadowRadius, mCollapsedShadowRadius, fraction, null), lerp(mExpandedShadowDx, mCollapsedShadowDx, fraction, null), lerp(mExpandedShadowDy, mCollapsedShadowDy, fraction, null), blendColors(mExpandedShadowColor, mCollapsedShadowColor, fraction));
        ViewCompat.postInvalidateOnAnimation(mView);
    }

    private void calculateBaseOffsets() {
        final float currentTextSize = mCurrentTextSize;
        // We then calculate the collapsed text size, using the same logic
        calculateUsingTextSize(mCollapsedTextSize);
        float width = mTextToDraw != null ? mTextPaint.measureText(mTextToDraw, 0, mTextToDraw.length()) : 0;
        final int collapsedAbsGravity = GravityCompat.getAbsoluteGravity(mCollapsedTextGravity, mIsRtl ? ViewCompat.LAYOUT_DIRECTION_RTL : ViewCompat.LAYOUT_DIRECTION_LTR);
        switch(collapsedAbsGravity & Gravity.VERTICAL_GRAVITY_MASK) {
            case Gravity.BOTTOM:
                mCollapsedDrawY = mCollapsedBounds.bottom;
                break;
            case Gravity.TOP:
                mCollapsedDrawY = mCollapsedBounds.top - mTextPaint.ascent();
                break;
            case Gravity.CENTER_VERTICAL:
            default:
                float textHeight = mTextPaint.descent() - mTextPaint.ascent();
                float textOffset = (textHeight / 2) - mTextPaint.descent();
                mCollapsedDrawY = mCollapsedBounds.centerY() + textOffset;
                break;
        }
        switch(collapsedAbsGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
            case Gravity.CENTER_HORIZONTAL:
                mCollapsedDrawX = mCollapsedBounds.centerX() - (width / 2);
                break;
            case Gravity.RIGHT:
                mCollapsedDrawX = mCollapsedBounds.right - width;
                break;
            case Gravity.LEFT:
            default:
                mCollapsedDrawX = mCollapsedBounds.left;
                break;
        }
        calculateUsingTextSize(mExpandedTextSize);
        width = mTextToDraw != null ? mTextPaint.measureText(mTextToDraw, 0, mTextToDraw.length()) : 0;
        final int expandedAbsGravity = GravityCompat.getAbsoluteGravity(mExpandedTextGravity, mIsRtl ? ViewCompat.LAYOUT_DIRECTION_RTL : ViewCompat.LAYOUT_DIRECTION_LTR);
        switch(expandedAbsGravity & Gravity.VERTICAL_GRAVITY_MASK) {
            case Gravity.BOTTOM:
                mExpandedDrawY = mExpandedBounds.bottom;
                break;
            case Gravity.TOP:
                mExpandedDrawY = mExpandedBounds.top - mTextPaint.ascent();
                break;
            case Gravity.CENTER_VERTICAL:
            default:
                float textHeight = mTextPaint.descent() - mTextPaint.ascent();
                float textOffset = (textHeight / 2) - mTextPaint.descent();
                mExpandedDrawY = mExpandedBounds.centerY() + textOffset;
                break;
        }
        switch(expandedAbsGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
            case Gravity.CENTER_HORIZONTAL:
                mExpandedDrawX = mExpandedBounds.centerX() - (width / 2);
                break;
            case Gravity.RIGHT:
                mExpandedDrawX = mExpandedBounds.right - width;
                break;
            case Gravity.LEFT:
            default:
                mExpandedDrawX = mExpandedBounds.left;
                break;
        }
        // The bounds have changed so we need to clear the texture
        clearTexture();
        // Now reset the text size back to the original
        setInterpolatedTextSize(currentTextSize);
    }

    private void interpolateBounds(float fraction) {
        mCurrentBounds.left = lerp(mExpandedBounds.left, mCollapsedBounds.left, fraction, mPositionInterpolator);
        mCurrentBounds.top = lerp(mExpandedDrawY, mCollapsedDrawY, fraction, mPositionInterpolator);
        mCurrentBounds.right = lerp(mExpandedBounds.right, mCollapsedBounds.right, fraction, mPositionInterpolator);
        mCurrentBounds.bottom = lerp(mExpandedBounds.bottom, mCollapsedBounds.bottom, fraction, mPositionInterpolator);
    }

    public void draw(Canvas canvas) {
        final int saveCount = canvas.save();
        if (mTextToDraw != null && mDrawTitle) {
            float x = mCurrentDrawX;
            float y = mCurrentDrawY;
            final boolean drawTexture = mUseTexture && mExpandedTitleTexture != null;
            final float ascent;
            final float descent;
            // Update the TextPaint to the current text size
            mTextPaint.setTextSize(mCurrentTextSize);
            if (drawTexture) {
                ascent = mTextureAscent * mScale;
                descent = mTextureDescent * mScale;
            } else {
                ascent = mTextPaint.ascent() * mScale;
                descent = mTextPaint.descent() * mScale;
            }
            if (DEBUG_DRAW) {
                // Just a debug tool, which drawn a Magneta rect in the text bounds
                canvas.drawRect(mCurrentBounds.left, y + ascent, mCurrentBounds.right, y + descent, DEBUG_DRAW_PAINT);
            }
            if (drawTexture) {
                y += ascent;
            }
            if (mScale != 1f) {
                canvas.scale(mScale, mScale, x, y);
            }
            if (drawTexture) {
                // If we should use a texture, draw it instead of text
                canvas.drawBitmap(mExpandedTitleTexture, x, y, mTexturePaint);
            } else {
                canvas.drawText(mTextToDraw, 0, mTextToDraw.length(), x, y, mTextPaint);
            }
        }
        canvas.restoreToCount(saveCount);
    }

    private boolean calculateIsRtl(CharSequence text) {
        final boolean defaultIsRtl = ViewCompat.getLayoutDirection(mView) == ViewCompat.LAYOUT_DIRECTION_RTL;
        return (defaultIsRtl ? TextDirectionHeuristicsCompat.FIRSTSTRONG_RTL : TextDirectionHeuristicsCompat.FIRSTSTRONG_LTR).isRtl(text, 0, text.length());
    }

    private void setInterpolatedTextSize(float textSize) {
        calculateUsingTextSize(textSize);
        // Use our texture if the scale isn't 1.0
        mUseTexture = USE_SCALING_TEXTURE && mScale != 1f;
        if (mUseTexture) {
            // Make sure we have an expanded texture if needed
            ensureExpandedTexture();
        }
        ViewCompat.postInvalidateOnAnimation(mView);
    }

    private void calculateUsingTextSize(final float textSize) {
        if (mText == null)
            return;
        final float availableWidth;
        final float newTextSize;
        boolean updateDrawText = false;
        if (isClose(textSize, mCollapsedTextSize)) {
            availableWidth = mCollapsedBounds.width();
            newTextSize = mCollapsedTextSize;
            mScale = 1f;
            if (mCurrentTypeface != mCollapsedTypeface) {
                mCurrentTypeface = mCollapsedTypeface;
                updateDrawText = true;
            }
        } else {
            availableWidth = mExpandedBounds.width();
            newTextSize = mExpandedTextSize;
            if (mCurrentTypeface != mExpandedTypeface) {
                mCurrentTypeface = mExpandedTypeface;
                updateDrawText = true;
            }
            if (isClose(textSize, mExpandedTextSize)) {
                // If we're close to the expanded text size, snap to it and use a scale of 1
                mScale = 1f;
            } else {
                // Else, we'll scale down from the expanded text size
                mScale = textSize / mExpandedTextSize;
            }
        }
        if (availableWidth > 0) {
            updateDrawText = (mCurrentTextSize != newTextSize) || mBoundsChanged || updateDrawText;
            mCurrentTextSize = newTextSize;
            mBoundsChanged = false;
        }
        if (mTextToDraw == null || updateDrawText) {
            mTextPaint.setTextSize(mCurrentTextSize);
            mTextPaint.setTypeface(mCurrentTypeface);
            // If we don't currently have text to draw, or the text size has changed, ellipsize...
            final CharSequence title = TextUtils.ellipsize(mText, mTextPaint, availableWidth, TextUtils.TruncateAt.END);
            if (!TextUtils.equals(title, mTextToDraw)) {
                mTextToDraw = title;
                mIsRtl = calculateIsRtl(mTextToDraw);
            }
        }
    }

    private void ensureExpandedTexture() {
        if (mExpandedTitleTexture != null || mExpandedBounds.isEmpty() || TextUtils.isEmpty(mTextToDraw)) {
            return;
        }
        calculateOffsets(0f);
        mTextureAscent = mTextPaint.ascent();
        mTextureDescent = mTextPaint.descent();
        final int w = Math.round(mTextPaint.measureText(mTextToDraw, 0, mTextToDraw.length()));
        final int h = Math.round(mTextureDescent - mTextureAscent);
        if (w <= 0 && h <= 0) {
            // If the width or height are 0, return
            return;
        }
        mExpandedTitleTexture = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
        Canvas c = new Canvas(mExpandedTitleTexture);
        c.drawText(mTextToDraw, 0, mTextToDraw.length(), 0, h - mTextPaint.descent(), mTextPaint);
        if (mTexturePaint == null) {
            // Make sure we have a paint
            mTexturePaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
        }
    }

    public void recalculate() {
        if (mView.getHeight() > 0 && mView.getWidth() > 0) {
            // If we've already been laid out, calculate everything now otherwise we'll wait
            // until a layout
            calculateBaseOffsets();
            calculateCurrentOffsets();
        }
    }

    /**
     * Set the title to display
     *
     * @param text
     */
    void setText(CharSequence text) {
        if (text == null || !text.equals(mText)) {
            mText = text;
            mTextToDraw = null;
            clearTexture();
            recalculate();
        }
    }

    CharSequence getText() {
        return mText;
    }

    private void clearTexture() {
        if (mExpandedTitleTexture != null) {
            mExpandedTitleTexture.recycle();
            mExpandedTitleTexture = null;
        }
    }

    /**
     * Returns true if {@code value} is 'close' to it's closest decimal value. Close is currently
     * defined as it's difference being < 0.001.
     */

    int getExpandedTextColor() {
        return mExpandedTextColor;
    }

    int getCollapsedTextColor() {
        return mCollapsedTextColor;
    }

    /**
     * Blend {@code color1} and {@code color2} using the given ratio.
     *
     * @param ratio of which to blend. 0.0 will return {@code color1}, 0.5 will give an even blend,
     *              1.0 will return {@code color2}.
     */
    private static int blendColors(int color1, int color2, float ratio) {
        final float inverseRatio = 1f - ratio;
        float a = (Color.alpha(color1) * inverseRatio) + (Color.alpha(color2) * ratio);
        float r = (Color.red(color1) * inverseRatio) + (Color.red(color2) * ratio);
        float g = (Color.green(color1) * inverseRatio) + (Color.green(color2) * ratio);
        float b = (Color.blue(color1) * inverseRatio) + (Color.blue(color2) * ratio);
        return Color.argb((int) a, (int) r, (int) g, (int) b);
    }

    private static float lerp(float startValue, float endValue, float fraction, Interpolator interpolator) {
        if (interpolator != null) {
            fraction = interpolator.getInterpolation(fraction);
        }
        return AnimationUtils.lerp(startValue, endValue, fraction);
    }

    private static boolean rectEquals(Rect r, int left, int top, int right, int bottom) {
        return !(r.left != left || r.top != top || r.right != right || r.bottom != bottom);
    }
}


switch(collapsedAbsGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
            case Gravity.CENTER_HORIZONTAL:
                mCollapsedDrawX = mCollapsedBounds.centerX() - (width / 2);
                break;
            case Gravity.RIGHT:
                mCollapsedDrawX = mCollapsedBounds.right - width;
                break;
            case Gravity.LEFT:
            default:
                mCollapsedDrawX = mCollapsedBounds.left;
                break;
        }

package ticwear.design.widget;

import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Typeface;
import android.os.Build;
import android.support.v4.text.TextDirectionHeuristicsCompat;
import android.support.v4.view.GravityCompat;
import android.support.v4.view.ViewCompat;
import android.text.TextPaint;
import android.text.TextUtils;
import android.view.Gravity;
import android.view.View;
import android.view.animation.Interpolator;
import ticwear.design.R;

final class CollapsingTextHelper {

    // Pre-JB-MR2 doesn't support HW accelerated canvas scaled text so we will workaround it
    // by using our own texture
    private static final boolean USE_SCALING_TEXTURE = Build.VERSION.SDK_INT < 18;

    private static final boolean DEBUG_DRAW = false;

    private static final Paint DEBUG_DRAW_PAINT;

    static {
        DEBUG_DRAW_PAINT = DEBUG_DRAW ? new Paint() : null;
        if (DEBUG_DRAW_PAINT != null) {
            DEBUG_DRAW_PAINT.setAntiAlias(true);
            DEBUG_DRAW_PAINT.setColor(Color.MAGENTA);
        }
    }

    private final View mView;

    private boolean mDrawTitle;

    private float mExpandedFraction;

    private final Rect mExpandedBounds;

    private final Rect mCollapsedBounds;

    private final RectF mCurrentBounds;

    private int mExpandedTextGravity = Gravity.CENTER_VERTICAL;

    private int mCollapsedTextGravity = Gravity.CENTER_VERTICAL;

    private float mExpandedTextSize = 15;

    private float mCollapsedTextSize = 15;

    private int mExpandedTextColor;

    private int mCollapsedTextColor;

    private float mExpandedDrawY;

    private float mCollapsedDrawY;

    private float mExpandedDrawX;

    private float mCollapsedDrawX;

    private float mCurrentDrawX;

    private float mCurrentDrawY;

    private Typeface mCollapsedTypeface;

    private Typeface mExpandedTypeface;

    private Typeface mCurrentTypeface;

    private CharSequence mText;

    private CharSequence mTextToDraw;

    private boolean mIsRtl;

    private boolean mUseTexture;

    private Bitmap mExpandedTitleTexture;

    private Paint mTexturePaint;

    private float mTextureAscent;

    private float mTextureDescent;

    private float mScale;

    private float mCurrentTextSize;

    private boolean mBoundsChanged;

    private final TextPaint mTextPaint;

    private Interpolator mPositionInterpolator;

    private Interpolator mTextSizeInterpolator;

    private float mCollapsedShadowRadius, mCollapsedShadowDx, mCollapsedShadowDy;

    private int mCollapsedShadowColor;

    private float mExpandedShadowRadius, mExpandedShadowDx, mExpandedShadowDy;

    private int mExpandedShadowColor;

    public CollapsingTextHelper(View view) {
        mView = view;
        mTextPaint = new TextPaint();
        mTextPaint.setAntiAlias(true);
        mCollapsedBounds = new Rect();
        mExpandedBounds = new Rect();
        mCurrentBounds = new RectF();
    }

    void setTextSizeInterpolator(Interpolator interpolator) {
        mTextSizeInterpolator = interpolator;
        recalculate();
    }

    void setPositionInterpolator(Interpolator interpolator) {
        mPositionInterpolator = interpolator;
        recalculate();
    }

    void setExpandedTextSize(float textSize) {
        if (mExpandedTextSize != textSize) {
            mExpandedTextSize = textSize;
            recalculate();
        }
    }

    void setCollapsedTextSize(float textSize) {
        if (mCollapsedTextSize != textSize) {
            mCollapsedTextSize = textSize;
            recalculate();
        }
    }

    void setCollapsedTextColor(int textColor) {
        if (mCollapsedTextColor != textColor) {
            mCollapsedTextColor = textColor;
            recalculate();
        }
    }

    void setExpandedTextColor(int textColor) {
        if (mExpandedTextColor != textColor) {
            mExpandedTextColor = textColor;
            recalculate();
        }
    }

    void setExpandedBounds(int left, int top, int right, int bottom) {
        if (!rectEquals(mExpandedBounds, left, top, right, bottom)) {
            mExpandedBounds.set(left, top, right, bottom);
            mBoundsChanged = true;
            onBoundsChanged();
        }
    }

    void setCollapsedBounds(int left, int top, int right, int bottom) {
        if (!rectEquals(mCollapsedBounds, left, top, right, bottom)) {
            mCollapsedBounds.set(left, top, right, bottom);
            mBoundsChanged = true;
            onBoundsChanged();
        }
    }

    void onBoundsChanged() {
        mDrawTitle = mCollapsedBounds.width() > 0 && mCollapsedBounds.height() > 0 && mExpandedBounds.width() > 0 && mExpandedBounds.height() > 0;
    }

    void setExpandedTextGravity(int gravity) {
        if (mExpandedTextGravity != gravity) {
            mExpandedTextGravity = gravity;
            recalculate();
        }
    }

    int getExpandedTextGravity() {
        return mExpandedTextGravity;
    }

    void setCollapsedTextGravity(int gravity) {
        if (mCollapsedTextGravity != gravity) {
            mCollapsedTextGravity = gravity;
            recalculate();
        }
    }

    int getCollapsedTextGravity() {
        return mCollapsedTextGravity;
    }

    void setCollapsedTextAppearance(int resId) {
        TypedArray a = mView.getContext().obtainStyledAttributes(resId, R.styleable.TextAppearance);
        if (a.hasValue(R.styleable.TextAppearance_android_textColor)) {
            mCollapsedTextColor = a.getColor(R.styleable.TextAppearance_android_textColor, mCollapsedTextColor);
        }
        if (a.hasValue(R.styleable.TextAppearance_android_textSize)) {
            mCollapsedTextSize = a.getDimensionPixelSize(R.styleable.TextAppearance_android_textSize, (int) mCollapsedTextSize);
        }
        mCollapsedShadowColor = a.getInt(R.styleable.TextAppearance_android_shadowColor, 0);
        mCollapsedShadowDx = a.getFloat(R.styleable.TextAppearance_android_shadowDx, 0);
        mCollapsedShadowDy = a.getFloat(R.styleable.TextAppearance_android_shadowDy, 0);
        mCollapsedShadowRadius = a.getFloat(R.styleable.TextAppearance_android_shadowRadius, 0);
        a.recycle();
        if (Build.VERSION.SDK_INT >= 16) {
            mCollapsedTypeface = readFontFamilyTypeface(resId);
        }
        recalculate();
    }

    void setExpandedTextAppearance(int resId) {
        TypedArray a = mView.getContext().obtainStyledAttributes(resId, R.styleable.TextAppearance);
        if (a.hasValue(R.styleable.TextAppearance_android_textColor)) {
            mExpandedTextColor = a.getColor(R.styleable.TextAppearance_android_textColor, mExpandedTextColor);
        }
        if (a.hasValue(R.styleable.TextAppearance_android_textSize)) {
            mExpandedTextSize = a.getDimensionPixelSize(R.styleable.TextAppearance_android_textSize, (int) mExpandedTextSize);
        }
        mExpandedShadowColor = a.getInt(R.styleable.TextAppearance_android_shadowColor, 0);
        mExpandedShadowDx = a.getFloat(R.styleable.TextAppearance_android_shadowDx, 0);
        mExpandedShadowDy = a.getFloat(R.styleable.TextAppearance_android_shadowDy, 0);
        mExpandedShadowRadius = a.getFloat(R.styleable.TextAppearance_android_shadowRadius, 0);
        a.recycle();
        if (Build.VERSION.SDK_INT >= 16) {
            mExpandedTypeface = readFontFamilyTypeface(resId);
        }
        recalculate();
    }

    private Typeface readFontFamilyTypeface(int resId) {
        final TypedArray a = mView.getContext().obtainStyledAttributes(resId, new int[] { android.R.attr.fontFamily });
        try {
            final String family = a.getString(0);
            if (family != null) {
                return Typeface.create(family, Typeface.NORMAL);
            }
        } finally {
            a.recycle();
        }
        return null;
    }

    void setCollapsedTypeface(Typeface typeface) {
        if (mCollapsedTypeface != typeface) {
            mCollapsedTypeface = typeface;
            recalculate();
        }
    }

    void setExpandedTypeface(Typeface typeface) {
        if (mExpandedTypeface != typeface) {
            mExpandedTypeface = typeface;
            recalculate();
        }
    }

    void setTypefaces(Typeface typeface) {
        mCollapsedTypeface = mExpandedTypeface = typeface;
        recalculate();
    }

    Typeface getCollapsedTypeface() {
        return mCollapsedTypeface != null ? mCollapsedTypeface : Typeface.DEFAULT;
    }

    Typeface getExpandedTypeface() {
        return mExpandedTypeface != null ? mExpandedTypeface : Typeface.DEFAULT;
    }

    /**
     * Set the value indicating the current scroll value. This decides how much of the
     * background will be displayed, as well as the title metrics/positioning.
     *
     * A value of {@code 0.0} indicates that the layout is fully expanded.
     * A value of {@code 1.0} indicates that the layout is fully collapsed.
     */
    void setExpansionFraction(float fraction) {
        fraction = MathUtils.constrain(fraction, 0f, 1f);
        if (fraction != mExpandedFraction) {
            mExpandedFraction = fraction;
            calculateCurrentOffsets();
        }
    }

    float getExpansionFraction() {
        return mExpandedFraction;
    }

    float getCollapsedTextSize() {
        return mCollapsedTextSize;
    }

    float getExpandedTextSize() {
        return mExpandedTextSize;
    }

    private void calculateCurrentOffsets() {
        calculateOffsets(mExpandedFraction);
    }

    private void calculateOffsets(final float fraction) {
        interpolateBounds(fraction);
        mCurrentDrawX = lerp(mExpandedDrawX, mCollapsedDrawX, fraction, mPositionInterpolator);
        mCurrentDrawY = lerp(mExpandedDrawY, mCollapsedDrawY, fraction, mPositionInterpolator);
        setInterpolatedTextSize(lerp(mExpandedTextSize, mCollapsedTextSize, fraction, mTextSizeInterpolator));
        if (mCollapsedTextColor != mExpandedTextColor) {
            // If the collapsed and expanded text colors are different, blend them based on the
            // fraction
            mTextPaint.setColor(blendColors(mExpandedTextColor, mCollapsedTextColor, fraction));
        } else {
            mTextPaint.setColor(mCollapsedTextColor);
        }
        mTextPaint.setShadowLayer(lerp(mExpandedShadowRadius, mCollapsedShadowRadius, fraction, null), lerp(mExpandedShadowDx, mCollapsedShadowDx, fraction, null), lerp(mExpandedShadowDy, mCollapsedShadowDy, fraction, null), blendColors(mExpandedShadowColor, mCollapsedShadowColor, fraction));
        ViewCompat.postInvalidateOnAnimation(mView);
    }

    private void calculateBaseOffsets() {
        final float currentTextSize = mCurrentTextSize;
        // We then calculate the collapsed text size, using the same logic
        calculateUsingTextSize(mCollapsedTextSize);
        float width = mTextToDraw != null ? mTextPaint.measureText(mTextToDraw, 0, mTextToDraw.length()) : 0;
        final int collapsedAbsGravity = GravityCompat.getAbsoluteGravity(mCollapsedTextGravity, mIsRtl ? ViewCompat.LAYOUT_DIRECTION_RTL : ViewCompat.LAYOUT_DIRECTION_LTR);
        switch(collapsedAbsGravity & Gravity.VERTICAL_GRAVITY_MASK) {
            case Gravity.BOTTOM:
                mCollapsedDrawY = mCollapsedBounds.bottom;
                break;
            case Gravity.TOP:
                mCollapsedDrawY = mCollapsedBounds.top - mTextPaint.ascent();
                break;
            case Gravity.CENTER_VERTICAL:
            default:
                float textHeight = mTextPaint.descent() - mTextPaint.ascent();
                float textOffset = (textHeight / 2) - mTextPaint.descent();
                mCollapsedDrawY = mCollapsedBounds.centerY() + textOffset;
                break;
        }

        calculateUsingTextSize(mExpandedTextSize);
        width = mTextToDraw != null ? mTextPaint.measureText(mTextToDraw, 0, mTextToDraw.length()) : 0;
        final int expandedAbsGravity = GravityCompat.getAbsoluteGravity(mExpandedTextGravity, mIsRtl ? ViewCompat.LAYOUT_DIRECTION_RTL : ViewCompat.LAYOUT_DIRECTION_LTR);
        switch(expandedAbsGravity & Gravity.VERTICAL_GRAVITY_MASK) {
            case Gravity.BOTTOM:
                mExpandedDrawY = mExpandedBounds.bottom;
                break;
            case Gravity.TOP:
                mExpandedDrawY = mExpandedBounds.top - mTextPaint.ascent();
                break;
            case Gravity.CENTER_VERTICAL:
            default:
                float textHeight = mTextPaint.descent() - mTextPaint.ascent();
                float textOffset = (textHeight / 2) - mTextPaint.descent();
                mExpandedDrawY = mExpandedBounds.centerY() + textOffset;
                break;
        }
        switch(expandedAbsGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
            case Gravity.CENTER_HORIZONTAL:
                mExpandedDrawX = mExpandedBounds.centerX() - (width / 2);
                break;
            case Gravity.RIGHT:
                mExpandedDrawX = mExpandedBounds.right - width;
                break;
            case Gravity.LEFT:
            default:
                mExpandedDrawX = mExpandedBounds.left;
                break;
        }
        // The bounds have changed so we need to clear the texture
        clearTexture();
        // Now reset the text size back to the original
        setInterpolatedTextSize(currentTextSize);
    }

    private void interpolateBounds(float fraction) {
        mCurrentBounds.left = lerp(mExpandedBounds.left, mCollapsedBounds.left, fraction, mPositionInterpolator);
        mCurrentBounds.top = lerp(mExpandedDrawY, mCollapsedDrawY, fraction, mPositionInterpolator);
        mCurrentBounds.right = lerp(mExpandedBounds.right, mCollapsedBounds.right, fraction, mPositionInterpolator);
        mCurrentBounds.bottom = lerp(mExpandedBounds.bottom, mCollapsedBounds.bottom, fraction, mPositionInterpolator);
    }

    public void draw(Canvas canvas) {
        final int saveCount = canvas.save();
        if (mTextToDraw != null && mDrawTitle) {
            float x = mCurrentDrawX;
            float y = mCurrentDrawY;
            final boolean drawTexture = mUseTexture && mExpandedTitleTexture != null;
            final float ascent;
            final float descent;
            // Update the TextPaint to the current text size
            mTextPaint.setTextSize(mCurrentTextSize);
            if (drawTexture) {
                ascent = mTextureAscent * mScale;
                descent = mTextureDescent * mScale;
            } else {
                ascent = mTextPaint.ascent() * mScale;
                descent = mTextPaint.descent() * mScale;
            }
            if (DEBUG_DRAW) {
                // Just a debug tool, which drawn a Magneta rect in the text bounds
                canvas.drawRect(mCurrentBounds.left, y + ascent, mCurrentBounds.right, y + descent, DEBUG_DRAW_PAINT);
            }
            if (drawTexture) {
                y += ascent;
            }
            if (mScale != 1f) {
                canvas.scale(mScale, mScale, x, y);
            }
            if (drawTexture) {
                // If we should use a texture, draw it instead of text
                canvas.drawBitmap(mExpandedTitleTexture, x, y, mTexturePaint);
            } else {
                canvas.drawText(mTextToDraw, 0, mTextToDraw.length(), x, y, mTextPaint);
            }
        }
        canvas.restoreToCount(saveCount);
    }

    private boolean calculateIsRtl(CharSequence text) {
        final boolean defaultIsRtl = ViewCompat.getLayoutDirection(mView) == ViewCompat.LAYOUT_DIRECTION_RTL;
        return (defaultIsRtl ? TextDirectionHeuristicsCompat.FIRSTSTRONG_RTL : TextDirectionHeuristicsCompat.FIRSTSTRONG_LTR).isRtl(text, 0, text.length());
    }

    private void setInterpolatedTextSize(float textSize) {
        calculateUsingTextSize(textSize);
        // Use our texture if the scale isn't 1.0
        mUseTexture = USE_SCALING_TEXTURE && mScale != 1f;
        if (mUseTexture) {
            // Make sure we have an expanded texture if needed
            ensureExpandedTexture();
        }
        ViewCompat.postInvalidateOnAnimation(mView);
    }

    private void calculateUsingTextSize(final float textSize) {
        if (mText == null)
            return;
        final float availableWidth;
        final float newTextSize;
        boolean updateDrawText = false;
        if (isClose(textSize, mCollapsedTextSize)) {
            availableWidth = mCollapsedBounds.width();
            newTextSize = mCollapsedTextSize;
            mScale = 1f;
            if (mCurrentTypeface != mCollapsedTypeface) {
                mCurrentTypeface = mCollapsedTypeface;
                updateDrawText = true;
            }
        } else {
            availableWidth = mExpandedBounds.width();
            newTextSize = mExpandedTextSize;
            if (mCurrentTypeface != mExpandedTypeface) {
                mCurrentTypeface = mExpandedTypeface;
                updateDrawText = true;
            }
            if (isClose(textSize, mExpandedTextSize)) {
                // If we're close to the expanded text size, snap to it and use a scale of 1
                mScale = 1f;
            } else {
                // Else, we'll scale down from the expanded text size
                mScale = textSize / mExpandedTextSize;
            }
        }
        if (availableWidth > 0) {
            updateDrawText = (mCurrentTextSize != newTextSize) || mBoundsChanged || updateDrawText;
            mCurrentTextSize = newTextSize;
            mBoundsChanged = false;
        }
        if (mTextToDraw == null || updateDrawText) {
            mTextPaint.setTextSize(mCurrentTextSize);
            mTextPaint.setTypeface(mCurrentTypeface);
            // If we don't currently have text to draw, or the text size has changed, ellipsize...
            final CharSequence title = TextUtils.ellipsize(mText, mTextPaint, availableWidth, TextUtils.TruncateAt.END);
            if (!TextUtils.equals(title, mTextToDraw)) {
                mTextToDraw = title;
                mIsRtl = calculateIsRtl(mTextToDraw);
            }
        }
    }

    private void ensureExpandedTexture() {
        if (mExpandedTitleTexture != null || mExpandedBounds.isEmpty() || TextUtils.isEmpty(mTextToDraw)) {
            return;
        }
        calculateOffsets(0f);
        mTextureAscent = mTextPaint.ascent();
        mTextureDescent = mTextPaint.descent();
        final int w = Math.round(mTextPaint.measureText(mTextToDraw, 0, mTextToDraw.length()));
        final int h = Math.round(mTextureDescent - mTextureAscent);
        if (w <= 0 && h <= 0) {
            // If the width or height are 0, return
            return;
        }
        mExpandedTitleTexture = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
        Canvas c = new Canvas(mExpandedTitleTexture);
        c.drawText(mTextToDraw, 0, mTextToDraw.length(), 0, h - mTextPaint.descent(), mTextPaint);
        if (mTexturePaint == null) {
            // Make sure we have a paint
            mTexturePaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
        }
    }

    public void recalculate() {
        if (mView.getHeight() > 0 && mView.getWidth() > 0) {
            // If we've already been laid out, calculate everything now otherwise we'll wait
            // until a layout
            calculateBaseOffsets();
            calculateCurrentOffsets();
        }
    }

    /**
     * Set the title to display
     *
     * @param text
     */
    void setText(CharSequence text) {
        if (text == null || !text.equals(mText)) {
            mText = text;
            mTextToDraw = null;
            clearTexture();
            recalculate();
        }
    }

    CharSequence getText() {
        return mText;
    }

    private void clearTexture() {
        if (mExpandedTitleTexture != null) {
            mExpandedTitleTexture.recycle();
            mExpandedTitleTexture = null;
        }
    }

    /**
     * Returns true if {@code value} is 'close' to it's closest decimal value. Close is currently
     * defined as it's difference being < 0.001.
     */
    private static boolean isClose(float value, float targetValue) {
        return Math.abs(value - targetValue) < 0.001f;
    }

    int getExpandedTextColor() {
        return mExpandedTextColor;
    }

    int getCollapsedTextColor() {
        return mCollapsedTextColor;
    }

    /**
     * Blend {@code color1} and {@code color2} using the given ratio.
     *
     * @param ratio of which to blend. 0.0 will return {@code color1}, 0.5 will give an even blend,
     *              1.0 will return {@code color2}.
     */
    private static int blendColors(int color1, int color2, float ratio) {
        final float inverseRatio = 1f - ratio;
        float a = (Color.alpha(color1) * inverseRatio) + (Color.alpha(color2) * ratio);
        float r = (Color.red(color1) * inverseRatio) + (Color.red(color2) * ratio);
        float g = (Color.green(color1) * inverseRatio) + (Color.green(color2) * ratio);
        float b = (Color.blue(color1) * inverseRatio) + (Color.blue(color2) * ratio);
        return Color.argb((int) a, (int) r, (int) g, (int) b);
    }

    private static float lerp(float startValue, float endValue, float fraction, Interpolator interpolator) {
        if (interpolator != null) {
            fraction = interpolator.getInterpolation(fraction);
        }
        return AnimationUtils.lerp(startValue, endValue, fraction);
    }

    private static boolean rectEquals(Rect r, int left, int top, int right, int bottom) {
        return !(r.left != left || r.top != top || r.right != right || r.bottom != bottom);
    }
}