/*
 * The Kuali Financial System, a comprehensive financial management system for higher education.
 *
 * Copyright 2005-2016 The Kuali Foundation
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.kuali.kfs.sys.batch.service;

import org.apache.commons.lang.StringUtils;
import org.kuali.kfs.krad.bo.PersistableBusinessObjectBase;
import org.kuali.kfs.krad.util.ObjectUtils;
import org.kuali.rice.krad.bo.BusinessObject;
import org.springframework.transaction.annotation.Transactional;

import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * This class CANNOT be used by 2 processes simultaneously. It is for very specific batch processes that should not run at the same
 * time, and initialize and destroy must be called and the beginning and end of each process that uses it.
 */
@Transactional
public abstract class AbstractBatchTransactionalCachingService implements WrappingBatchService {
    private static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(AbstractBatchTransactionalCachingService.class);

    protected Map<String, BusinessObject> referenceValueCache;
    protected Map<Class, PreviousValueReference> previousValueCache;

    public void initialize() {
        referenceValueCache = new HashMap<String, BusinessObject>();
        previousValueCache = new HashMap<Class, PreviousValueReference>();
    }

    public void destroy() {
        referenceValueCache = null;
        previousValueCache = null;
    }

    static final class NonExistentReferenceBusinessObject extends PersistableBusinessObjectBase {

        protected LinkedHashMap toStringMapper_RICE20_REFACTORME() {
            throw new UnsupportedOperationException();
        }
    }

    protected static final BusinessObject NON_EXISTENT_REFERENCE_CACHE_VALUE = new NonExistentReferenceBusinessObject();

    protected String getCacheKey(Class clazz, Object... objects) {
        StringBuffer cacheKey = new StringBuffer(clazz.getName());
        for (int i = 0; i < objects.length; i++) {
            cacheKey.append("-").append(objects[i]);
        }
        return cacheKey.toString();
    }

    protected abstract class ReferenceValueRetriever<T extends BusinessObject> {
        public T get(Class<T> type, Object... keys) {
            String cacheKey = getCacheKey(type, keys);
            BusinessObject businessObject = referenceValueCache.get(cacheKey);
            if (businessObject == null) {
                try {
                    businessObject = useDao();
                } catch (Exception e) {
                    throw new RuntimeException("Unable to getBusinessObject in AccountingCycleCachingServiceImpl: " + cacheKey, e);
                }
                if (businessObject == null) {
                    referenceValueCache.put(cacheKey, NON_EXISTENT_REFERENCE_CACHE_VALUE);
                } else {
                    referenceValueCache.put(cacheKey, businessObject);
                    retrieveReferences((T) businessObject);
                }
            } else if (businessObject instanceof NonExistentReferenceBusinessObject) {
                businessObject = null;
            }
            return (T) businessObject;
        }

        protected abstract T useDao();

        protected abstract void retrieveReferences(T object);
    }

    public class PreviousValueReference<T extends BusinessObject> {
        protected String key = "";
        protected T value;

        public T getValue() {
            return value;
        }

        public void update(T value, String key) {
            this.key = key;
            this.value = value;
        }

        public void update(T value, Object... keys) {
            update(value, getCacheKey(value.getClass(), keys));
        }
    }
//    protected abstract class PreviousValueRetriever<T extends BusinessObject> {
//        public T get(Class<T> type, Object...keys) {
//            String cacheKey = getCacheKey(type, keys);
//            if (!cacheKey.equals(previousValueCache.get(AccountBalance.class).key.equals(cacheKey))) {
//                previousValueCache.get(type).update(useDao(), cacheKey);
//            }
//            return (T)previousValueCache.get(type).getValue();
//        }
//        protected abstract T useDao();
//    }

    protected abstract class PreviousValueRetriever<T extends BusinessObject> {
        public T get(Class<T> type, Object... keys) {
            // this should never happen, but in did, so just in case
            if (ObjectUtils.isNull(previousValueCache)) {
                LOG.error("previousValueCache is null. This shouldn't have happened.");
                previousValueCache = new HashMap<Class, PreviousValueReference>();
            }
            PreviousValueReference<T> pvr = previousValueCache.get(type);
            if (ObjectUtils.isNull(pvr)) {
                LOG.warn("PreviousValueReference for type " + type + " is not initialized; adding a new one to previousValueCache for it.");
                pvr = new PreviousValueReference<T>();
                previousValueCache.put(type, pvr);
            }
            String cacheKey = getCacheKey(type, keys);
            if (!StringUtils.equals(cacheKey, pvr.key)) {
                pvr.update(useDao(), cacheKey);
            }
            return pvr.getValue();
        }

        protected abstract T useDao();
    }
}
