/*
 * #%L
 * Protempa Framework
 * %%
 * Copyright (C) 2012 - 2013 Emory University
 * %%
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * #L%
 */
package org.protempa;

import org.protempa.valueset.ValueSet;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.logging.Level;
import org.apache.commons.collections4.map.ReferenceMap;
import org.apache.commons.lang3.StringUtils;
import org.arp.javautil.arrays.Arrays;
import org.protempa.backend.BackendInitializationException;
import org.protempa.backend.BackendNewInstanceException;
import org.protempa.backend.KnowledgeSourceBackendUpdatedEvent;
import org.protempa.backend.ksb.KnowledgeSourceBackend;
import org.protempa.query.And;

/**
 * A read-only "interface" to an externally-maintained knowledge base. The user
 * must specify a backend in the constructor from where information about
 * primitive parameters and abstract parameters can be obtained.
 *
 * @author Andrew Post
 */
public final class KnowledgeSourceImpl
        extends AbstractSource<KnowledgeSourceUpdatedEvent, KnowledgeSourceBackend, KnowledgeSourceUpdatedEvent, KnowledgeSourceBackendUpdatedEvent> implements KnowledgeSource {

    /**
     * PROTEMPA knowledge base.
     */
    private PropositionDefinitionCache propositionDefinitionCache;
    private final Map<String, Object> notFoundAbstractionDefinitionRequests;
    private final Map<String, Object> notFoundValueSetRequests;
    private final Map<String, Object> notFoundContextDefinitionRequests;
    private final Map<String, Object> notFoundPropositionDefinitionRequests;
    private final Map<String, Object> notFoundTemporalPropositionDefinitionRequests;
    private final PropositionDefinitionReader propDefReader;
    private final AbstractionDefinitionReader abstractionDefReader;
    private final ContextDefinitionReader contextDefReader;
    private final TemporalPropositionDefinitionReader tempPropDefReader;
    private final Map<PropositionDefinition, List<PropositionDefinition>> inverseIsACache;
    private final Map<AbstractionDefinition, List<PropositionDefinition>> abstractedFromCache;
    private final Map<ContextDefinition, List<ContextDefinition>> subContextsCache;
    private final Map<ContextDefinition, List<TemporalPropositionDefinition>> inducedByCache;
    private SubtreePropositionDefinitionGetterRegular inDataSourceGetter;
    private SubtreePropositionDefinitionGetterRegular collectSubtreeGetter;

    public KnowledgeSourceImpl(KnowledgeSourceBackend... backends) {
        super(backends);
        this.inverseIsACache = new ReferenceMap<>();
        this.abstractedFromCache = new ReferenceMap<>();
        this.subContextsCache = new ReferenceMap<>();
        this.inducedByCache = new ReferenceMap<>();
        this.notFoundAbstractionDefinitionRequests
                = new WeakHashMap<>();
        this.notFoundValueSetRequests = new WeakHashMap<>();
        this.notFoundPropositionDefinitionRequests
                = new WeakHashMap<>();
        this.notFoundContextDefinitionRequests
                = new WeakHashMap<>();
        this.notFoundTemporalPropositionDefinitionRequests
                = new HashMap<>();

        this.propDefReader = new PropositionDefinitionReader();
        this.abstractionDefReader = new AbstractionDefinitionReader();
        this.contextDefReader = new ContextDefinitionReader();
        this.tempPropDefReader = new TemporalPropositionDefinitionReader();
    }

    /**
     * Connect to the knowledge source backend(s).
     */
    private void initializeIfNeeded() throws BackendInitializationException,
            BackendNewInstanceException {
        if (isClosed()) {
            throw new IllegalStateException("Knowledge source already closed!");
        }
        if (this.propositionDefinitionCache == null) {
            this.propositionDefinitionCache = new PropositionDefinitionCache();
        }
        if (this.inDataSourceGetter == null) {
            this.inDataSourceGetter
                    = new SubtreePropositionDefinitionGetterRegular(this, true);
        }
        if (this.collectSubtreeGetter == null) {
            this.collectSubtreeGetter
                    = new SubtreePropositionDefinitionGetterRegular(this, false);
        }
    }

    private void initializeIfNeeded(String template, String... propIds)
            throws KnowledgeSourceReadException {
        try {
            initializeIfNeeded();
        } catch (BackendInitializationException | BackendNewInstanceException ex) {
            String action = MessageFormat.format(template, StringUtils.join(propIds, ", "));
            throw new KnowledgeSourceReadException(
                    "An error occurred " + action, ex);
        }
    }

    private void initializeIfNeeded(String action)
            throws KnowledgeSourceReadException {
        initializeIfNeeded(action, null);
    }

    @Override
    public List<PropositionDefinition> readInverseIsA(
            PropositionDefinition propDef)
            throws KnowledgeSourceReadException {
        if (propDef == null) {
            throw new IllegalArgumentException("propDef cannot be null");
        }
        synchronized (this.inverseIsACache) {
            List<PropositionDefinition> result = this.inverseIsACache.get(propDef);
            if (result != null) {
                return result;
            } else {
                result = new ArrayList<>();
                initializeIfNeeded("reading inverseIsA of {0}", propDef.getId());
                String[] propIds = propDef.getInverseIsA();
                for (String propId : propIds) {
                    PropositionDefinition pd = readPropositionDefinition(propId);
                    assert pd != null :
                            "Proposition definition " + propId
                            + ", which is specified as a child of "
                            + propDef.getId() + ", does not exist";
                    if (pd != null) {
                        result.add(pd);
                    }
                }
                result = Collections.unmodifiableList(result);
                this.inverseIsACache.put(propDef, result);
                return result;
            }
        }
    }

    @Override
    public List<PropositionDefinition> readIsA(PropositionDefinition propDef)
            throws KnowledgeSourceReadException {
        if (propDef == null) {
            throw new IllegalArgumentException("propDef cannot be null");
        }

        return readIsA(propDef.getId());
    }

    @Override
    public List<PropositionDefinition> readInverseIsA(String id)
            throws KnowledgeSourceReadException {
        if (id == null) {
            throw new IllegalArgumentException("id cannot be null");
        }
        PropositionDefinition propDef = readPropositionDefinition(id);
        if (propDef == null) {
            return Collections.emptyList();
        } else {
            return readInverseIsA(propDef);
        }
    }

    @Override
    public List<PropositionDefinition> readIsA(String id)
            throws KnowledgeSourceReadException {
        if (id == null) {
            throw new IllegalArgumentException("id cannot be null");
        }
        Set<String> isAs = new HashSet<>();
        for (KnowledgeSourceBackend backend : getBackends()) {
            Arrays.addAll(isAs, backend.readIsA(id));
        }
        List<PropositionDefinition> result
                = new ArrayList<>();
        for (String isAPropId : isAs) {
            result.add(readPropositionDefinition(isAPropId));
        }

        return result;
    }

    @Override
    public List<PropositionDefinition> readAbstractedFrom(
            AbstractionDefinition propDef)
            throws KnowledgeSourceReadException {
        if (propDef == null) {
            throw new IllegalArgumentException("propDef cannot be null");
        }
        synchronized (this.abstractedFromCache) {
            List<PropositionDefinition> result
                    = this.abstractedFromCache.get(propDef);
            if (result != null) {
                return result;
            } else {
                result = new ArrayList<>();
                initializeIfNeeded("reading abstractedFrom of {0}",
                        propDef.getId());

                Set<String> propIds = propDef.getAbstractedFrom();
                for (String propId : propIds) {
                    PropositionDefinition pd = readPropositionDefinition(propId);
                    assert pd != null :
                            "Proposition definition " + propId
                            + ", which " + propDef.getId()
                            + "is specified as abstracted from, does not exist";
                    if (pd != null) {
                        result.add(pd);
                    }
                }
                result = Collections.unmodifiableList(result);
                if (propDef != null) {
                    this.abstractedFromCache.put(propDef, result);
                }
                return result;
            }
        }
    }

    @Override
    public List<AbstractionDefinition> readAbstractedInto(
            PropositionDefinition propDef)
            throws KnowledgeSourceReadException {
        if (propDef == null) {
            throw new IllegalArgumentException("propDef cannot be null");
        }

        return readAbstractedInto(propDef.getId());
    }

    @Override
    public List<PropositionDefinition> readAbstractedFrom(String id)
            throws KnowledgeSourceReadException {
        if (id == null) {
            throw new IllegalArgumentException("id cannot be null");
        }
        AbstractionDefinition propDef = readAbstractionDefinition(id);
        if (propDef == null) {
            return Collections.emptyList();
        } else {
            return readAbstractedFrom(propDef);
        }
    }

    @Override
    public List<AbstractionDefinition> readAbstractedInto(String id)
            throws KnowledgeSourceReadException {
        if (id == null) {
            throw new IllegalArgumentException("id cannot be null");
        }
        Set<String> abstractedIntos = new HashSet<>();
        for (KnowledgeSourceBackend backend : getBackends()) {
            Arrays.addAll(abstractedIntos,
                    backend.readAbstractedInto(id));
        }
        List<AbstractionDefinition> result
                = new ArrayList<>();
        for (String abstractedIntoPropId : abstractedIntos) {
            AbstractionDefinition def
                    = readAbstractionDefinition(abstractedIntoPropId);
            if (def == null) {
                throw new KnowledgeSourceReadException(
                        "Invalid proposition id: " + abstractedIntoPropId);
            }
            result.add(def);
        }

        return result;
    }

    @Override
    public List<ContextDefinition> readInduces(
            TemporalPropositionDefinition propDef)
            throws KnowledgeSourceReadException {
        if (propDef == null) {
            throw new IllegalArgumentException("propDef cannot be null");
        }

        return readInduces(propDef.getId());
    }

    @Override
    public List<ContextDefinition> readInduces(String id)
            throws KnowledgeSourceReadException {
        if (id == null) {
            throw new IllegalArgumentException("id cannot be null");
        }
        Set<String> induces = new HashSet<>();
        for (KnowledgeSourceBackend backend : getBackends()) {
            Arrays.addAll(induces,
                    backend.readInduces(id));
        }
        List<ContextDefinition> result
                = new ArrayList<>();
        for (String inducesPropId : induces) {
            ContextDefinition def
                    = readContextDefinition(inducesPropId);
            if (def == null) {
                throw new KnowledgeSourceReadException(
                        "Invalid proposition id: " + inducesPropId);
            }
            result.add(def);
        }

        return result;
    }

    @Override
    public List<ContextDefinition> readSubContextOfs(String id)
            throws KnowledgeSourceReadException {
        if (id == null) {
            throw new IllegalArgumentException("id cannot be null");
        }
        Set<String> subContextOfs = new HashSet<>();
        for (KnowledgeSourceBackend backend : getBackends()) {
            Arrays.addAll(subContextOfs,
                    backend.readSubContextOfs(id));
        }
        List<ContextDefinition> result
                = new ArrayList<>();
        for (String subContextOfPropId : subContextOfs) {
            ContextDefinition def
                    = readContextDefinition(subContextOfPropId);
            if (def == null) {
                throw new KnowledgeSourceReadException(
                        "Invalid context id: " + subContextOfPropId);
            }
            result.add(def);
        }

        return result;
    }

    @Override
    public List<ContextDefinition> readSubContextOfs(ContextDefinition cd)
            throws KnowledgeSourceReadException {
        if (cd == null) {
            throw new IllegalArgumentException("cd cannot be null");
        }
        return readSubContextOfs(cd.getId());
    }

    @Override
    public List<ContextDefinition> readSubContexts(String contextId) throws KnowledgeSourceReadException {
        if (contextId == null) {
            throw new IllegalArgumentException("contextId cannot be null");
        }
        ContextDefinition pd = readContextDefinition(contextId);
        if (pd != null) {
            return readSubContexts(pd);
        } else {
            return new ArrayList(0);
        }

    }

    @Override
    public List<ContextDefinition> readSubContexts(ContextDefinition contextDef) throws KnowledgeSourceReadException {
        if (contextDef == null) {
            throw new IllegalArgumentException("contextDef cannot be null");
        }
        synchronized (this.subContextsCache) {
            List<ContextDefinition> result
                    = this.subContextsCache.get(contextDef);
            if (result != null) {
                return result;
            } else {
                result = new ArrayList<>();
                initializeIfNeeded("reading subContexts of {0}",
                        contextDef.getId());

                String[] propIds = contextDef.getSubContexts();
                for (String propId : propIds) {
                    ContextDefinition pd = readContextDefinition(propId);
                    assert pd != null :
                            "Context definition " + propId
                            + ", which " + contextDef.getId()
                            + "is specified as abstracted from, does not exist";
                    if (pd != null) {
                        result.add(pd);
                    }
                }
                result = Collections.unmodifiableList(result);
                if (contextDef != null) {
                    this.subContextsCache.put(contextDef, result);
                }
                return result;
            }
        }
    }

    @Override
    public List<PropositionDefinition> readParents(
            PropositionDefinition propDef)
            throws KnowledgeSourceReadException {
        if (propDef == null) {
            throw new IllegalArgumentException("propDef cannot be null");
        }
        List<ContextDefinition> subContextOfs;
        List<ContextDefinition> induces;
        if (propDef instanceof ContextDefinition) {
            subContextOfs = readSubContextOfs((ContextDefinition) propDef);
            induces = readInduces((ContextDefinition) propDef);
        } else {
            subContextOfs = Collections.emptyList();
            induces = Collections.emptyList();
        }

        List<AbstractionDefinition> ad = readAbstractedInto(propDef);
        List<PropositionDefinition> pd = readIsA(propDef);
        Map<String, PropositionDefinition> map
                = new HashMap<>();
        for (AbstractionDefinition def : ad) {
            assert def != null : "The abstractedInto list for " + propDef.getId() + " cannot have a null element";
            map.put(def.getId(), def);
        }
        for (PropositionDefinition def : pd) {
            assert def != null : "The abstractedInto list for " + propDef.getId() + " cannot have a null element";
            map.put(def.getId(), def);
        }
        for (ContextDefinition def : induces) {
            map.put(def.getId(), def);
        }
        for (ContextDefinition def : subContextOfs) {
            map.put(def.getId(), def);
        }
        return new ArrayList(map.values());
    }

    @Override
    public List<PropositionDefinition> readParents(String propId)
            throws KnowledgeSourceReadException {
        if (propId == null) {
            throw new IllegalArgumentException("propId cannot be null");
        }
        PropositionDefinition pd = readPropositionDefinition(propId);
        if (pd != null) {
            return readParents(pd);
        } else {
            return new ArrayList(0);
        }
    }

    @Override
    public List<TemporalPropositionDefinition> readInducedBy(String contextId) throws KnowledgeSourceReadException {
        if (contextId == null) {
            throw new IllegalArgumentException("contextId cannot be null");
        }
        ContextDefinition pd = readContextDefinition(contextId);
        if (pd != null) {
            return readInducedBy(pd);
        } else {
            return new ArrayList(0);
        }
    }

    @Override
    public List<TemporalPropositionDefinition> readInducedBy(ContextDefinition contextDef) throws KnowledgeSourceReadException {
        if (contextDef == null) {
            throw new IllegalArgumentException("contextDef cannot be null");
        }
        synchronized (this.inducedByCache) {
            List<TemporalPropositionDefinition> result
                    = this.inducedByCache.get(contextDef);
            if (result != null) {
                return result;
            } else {
                result = new ArrayList<>();
                initializeIfNeeded("reading inducedBy of {0}",
                        contextDef.getId());

                TemporalExtendedPropositionDefinition[] propIds = contextDef.getInducedBy();
                for (TemporalExtendedPropositionDefinition tepd : propIds) {
                    TemporalPropositionDefinition pd = readTemporalPropositionDefinition(tepd.getPropositionId());
                    assert pd != null :
                            "Proposition definition " + tepd.getPropositionId()
                            + ", which " + contextDef.getId()
                            + "is specified as induced by, does not exist";
                    if (pd != null) {
                        result.add(pd);
                    }
                }
                result = Collections.unmodifiableList(result);
                if (contextDef != null) {
                    this.inducedByCache.put(contextDef, result);
                }
                return result;
            }
        }
    }

    @Override
    public boolean hasTemporalPropositionDefinition(String id) throws KnowledgeSourceReadException {
        return this.tempPropDefReader.has(id);
    }

    @Override
    public TemporalPropositionDefinition readTemporalPropositionDefinition(String id) throws KnowledgeSourceReadException {
        return this.tempPropDefReader.read(id);
    }

    @Override
    public List<TemporalPropositionDefinition> readTemporalPropositionDefinitions(String... propIds) throws KnowledgeSourceReadException {
        return this.tempPropDefReader.read(propIds);
    }

    private abstract class AbstractDefinitionReader<E extends PropositionDefinition> {

        final E read(String id) throws KnowledgeSourceReadException {
            if (id == null) {
                throw new IllegalArgumentException("id cannot be null");
            }
            initializeIfNeeded("reading the proposition definition {0}", id);

            E result = null;
            synchronized (this) {
                if (!isInNotFound(id)) {
                    if (propositionDefinitionCache != null) {
                        result = readFromKnowledgeBase(id);
                        if (result == null) {
                            for (KnowledgeSourceBackend backend : getBackends()) {
                                result = readFromBackend(id, backend);
                                if (result != null) {
                                    try {
                                        putInKnowledgeBase(result);
                                    } catch (InvalidPropositionIdException ex) {
                                        ProtempaUtil.logger().log(Level.SEVERE, "Error adding proposition definition to cache", ex);
                                        throw new AssertionError("Error adding proposition definition to cache: " + ex.getMessage());
                                    }
                                    return result;
                                }
                            }
                            putInNotFound(id);
                        }
                    }
                }
            }
            return result;
        }

        final List<E> read(String[] ids) throws KnowledgeSourceReadException {
            assert ids != null : "ids cannot be null";
            initializeIfNeeded("reading the proposition definitions {0}", ids);

            List<E> result = new ArrayList<>();
            synchronized (this) {
                List<String> propIdsToFind = new ArrayList<>();
                for (String id : ids) {
                    if (!isInNotFound(id)) {
                        if (propositionDefinitionCache != null) {
                            E propDef = readFromKnowledgeBase(id);
                            if (propDef == null) {
                                propIdsToFind.add(id);
                            } else {
                                result.add(propDef);
                            }
                        }
                    }
                }

                List<E> resultNew = new ArrayList<>();
                for (KnowledgeSourceBackend backend : getBackends()) {
                    resultNew.addAll(readFromBackend(propIdsToFind, backend));
                }
                try {
                    putInKnowledgeBase(resultNew);
                } catch (InvalidPropositionIdException ex) {
                    throw new AssertionError("Error adding proposition definition to cache: " + ex.getMessage());
                }
                result.addAll(resultNew);
            }
            return result;
        }

        final boolean has(String id) throws KnowledgeSourceReadException {
            if (id == null) {
                throw new IllegalArgumentException("id cannot be null");
            }
            return read(id) != null;
        }

        protected abstract boolean isInNotFound(String id);

        protected abstract void putInNotFound(String id);

        protected abstract E readFromKnowledgeBase(String id);

        protected abstract E readFromBackend(String id,
                KnowledgeSourceBackend backend)
                throws KnowledgeSourceReadException;

        protected abstract List<E> readFromBackend(List<String> ids,
                KnowledgeSourceBackend backend)
                throws KnowledgeSourceReadException;

        protected abstract void putInKnowledgeBase(E propDef)
                throws InvalidPropositionIdException;

        void putInKnowledgeBase(List<E> propDefs)
                throws InvalidPropositionIdException {
            for (E propDef : propDefs) {
                putInKnowledgeBase(propDef);
            }
        }

    }

    private final class PropositionDefinitionReader extends AbstractDefinitionReader<PropositionDefinition> {

        @Override
        protected PropositionDefinition readFromKnowledgeBase(String id) {
            synchronized (propositionDefinitionCache) {
                return propositionDefinitionCache.getPropositionDefinition(id);
            }
        }

        @Override
        protected PropositionDefinition readFromBackend(String id,
                KnowledgeSourceBackend backend)
                throws KnowledgeSourceReadException {
            return backend.readPropositionDefinition(id);
        }

        @Override
        protected List<PropositionDefinition> readFromBackend(List<String> ids, KnowledgeSourceBackend backend) throws KnowledgeSourceReadException {
            return backend.readPropositionDefinitions(ids.toArray(new String[ids.size()]));
        }

        @Override
        protected boolean isInNotFound(String id) {
            return notFoundPropositionDefinitionRequests.containsKey(id);
        }

        @Override
        protected void putInNotFound(String id) {
            notFoundPropositionDefinitionRequests.put(id, null);
        }

        @Override
        protected void putInKnowledgeBase(PropositionDefinition propDef) throws InvalidPropositionIdException {
            synchronized (propositionDefinitionCache) {
                propositionDefinitionCache.addPropositionDefinition(propDef);
            }
        }

    }

    private final class AbstractionDefinitionReader
            extends AbstractDefinitionReader<AbstractionDefinition> {

        @Override
        protected AbstractionDefinition readFromKnowledgeBase(String id) {
            synchronized (propositionDefinitionCache) {
                AbstractionDefinition result
                        = propositionDefinitionCache.getAbstractionDefinition(id);
                if (result == null) {
                    PropositionDefinition r
                            = propositionDefinitionCache.getPropositionDefinition(id);
                    if (r instanceof AbstractionDefinition) {
                        try {
                            propositionDefinitionCache
                                    .addAbstractionDefinition((AbstractionDefinition) r);
                        } catch (InvalidPropositionIdException ex) {
                            throw new AssertionError("Should not happen");
                        }
                        result = (AbstractionDefinition) r;
                    }
                }
                return result;
            }
        }

        @Override
        protected AbstractionDefinition readFromBackend(String id,
                KnowledgeSourceBackend backend)
                throws KnowledgeSourceReadException {
            return backend.readAbstractionDefinition(id);
        }

        @Override
        protected List<AbstractionDefinition> readFromBackend(List<String> ids, KnowledgeSourceBackend backend) throws KnowledgeSourceReadException {
            return backend.readAbstractionDefinitions(ids.toArray(new String[ids.size()]));
        }

        @Override
        protected boolean isInNotFound(String id) {
            return notFoundAbstractionDefinitionRequests.containsKey(id);
        }

        @Override
        protected void putInNotFound(String id) {
            notFoundAbstractionDefinitionRequests.put(id, null);
        }

        @Override
        protected void putInKnowledgeBase(AbstractionDefinition propDef) throws InvalidPropositionIdException {
            synchronized (propositionDefinitionCache) {
                propositionDefinitionCache.addAbstractionDefinition(propDef);
                propositionDefinitionCache.addTemporalPropositionDefinition(propDef);
                propositionDefinitionCache.addPropositionDefinition(propDef);
            }
        }
    }

    private final class ContextDefinitionReader
            extends AbstractDefinitionReader<ContextDefinition> {

        @Override
        protected boolean isInNotFound(String id) {
            return notFoundContextDefinitionRequests.containsKey(id);
        }

        @Override
        protected void putInNotFound(String id) {
            notFoundContextDefinitionRequests.put(id, null);
        }

        @Override
        protected ContextDefinition readFromKnowledgeBase(String id) {
            synchronized (propositionDefinitionCache) {
                ContextDefinition result
                        = propositionDefinitionCache.getContextDefinition(id);
                if (result == null) {
                    PropositionDefinition r
                            = propositionDefinitionCache.getPropositionDefinition(id);
                    if (r instanceof ContextDefinition) {
                        try {
                            propositionDefinitionCache
                                    .addContextDefinition((ContextDefinition) r);
                        } catch (InvalidPropositionIdException ex) {
                            throw new AssertionError("Should not happen");
                        }
                        result = (ContextDefinition) r;
                    }
                }
                return result;
            }
        }

        @Override
        protected ContextDefinition readFromBackend(String id, KnowledgeSourceBackend backend) throws KnowledgeSourceReadException {
            return backend.readContextDefinition(id);
        }

        @Override
        protected List<ContextDefinition> readFromBackend(List<String> ids, KnowledgeSourceBackend backend) throws KnowledgeSourceReadException {
            return backend.readContextDefinitions(ids.toArray(new String[ids.size()]));
        }

        @Override
        protected void putInKnowledgeBase(ContextDefinition propDef) throws InvalidPropositionIdException {
            synchronized (propositionDefinitionCache) {
                propositionDefinitionCache.addContextDefinition(propDef);
                propositionDefinitionCache.addPropositionDefinition(propDef);
            }
        }
    }

    private final class TemporalPropositionDefinitionReader
            extends AbstractDefinitionReader<TemporalPropositionDefinition> {

        @Override
        protected boolean isInNotFound(String id) {
            return notFoundTemporalPropositionDefinitionRequests.containsKey(id);
        }

        @Override
        protected void putInNotFound(String id) {
            notFoundTemporalPropositionDefinitionRequests.put(id, null);
        }

        @Override
        protected TemporalPropositionDefinition readFromKnowledgeBase(String id) {
            synchronized (propositionDefinitionCache) {
                TemporalPropositionDefinition result
                        = propositionDefinitionCache.getTemporalPropositionDefinition(id);
                if (result == null) {
                    PropositionDefinition r
                            = propositionDefinitionCache.getPropositionDefinition(id);
                    if (r instanceof TemporalPropositionDefinition) {
                        try {
                            propositionDefinitionCache
                                    .addTemporalPropositionDefinition((TemporalPropositionDefinition) r);
                        } catch (InvalidPropositionIdException ex) {
                            throw new AssertionError("Should not happen");
                        }
                        result = (TemporalPropositionDefinition) r;
                    }
                }
                return result;
            }
        }

        @Override
        protected TemporalPropositionDefinition readFromBackend(String id, KnowledgeSourceBackend backend) throws KnowledgeSourceReadException {
            return backend.readTemporalPropositionDefinition(id);
        }

        @Override
        protected List<TemporalPropositionDefinition> readFromBackend(List<String> ids, KnowledgeSourceBackend backend) throws KnowledgeSourceReadException {
            return backend.readTemporalPropositionDefinitions(ids.toArray(new String[ids.size()]));
        }

        @Override
        protected void putInKnowledgeBase(TemporalPropositionDefinition propDef) throws InvalidPropositionIdException {
            synchronized (propositionDefinitionCache) {
                propositionDefinitionCache.addTemporalPropositionDefinition(propDef);
                propositionDefinitionCache.addPropositionDefinition(propDef);
            }
        }
    }

    /**
     * Returns the specified proposition definition.
     *
     * @param id a proposition id {@link String}. Cannot be <code>null</code>.
     * @return a {@link PropositionDefinition}, or <code>null</code> if none was
     * found with the given <code>id</code>.
     * @throws KnowledgeSourceReadException if an error occurred reading from
     * the knowledge base.
     */
    @Override
    public PropositionDefinition readPropositionDefinition(String id)
            throws KnowledgeSourceReadException {
        return this.propDefReader.read(id);
    }

    @Override
    public List<PropositionDefinition> readPropositionDefinitions(String... propIds)
            throws KnowledgeSourceReadException {
        return this.propDefReader.read(propIds);
    }

    @Override
    public boolean hasPropositionDefinition(String id)
            throws KnowledgeSourceReadException {
        return this.propDefReader.has(id);
    }

    @Override
    public AbstractionDefinition readAbstractionDefinition(String id) throws KnowledgeSourceReadException {
        return this.abstractionDefReader.read(id);
    }

    @Override
    public List<AbstractionDefinition> readAbstractionDefinitions(String... propIds) throws KnowledgeSourceReadException {
        return this.abstractionDefReader.read(propIds);
    }

    @Override
    public ContextDefinition readContextDefinition(String id) throws KnowledgeSourceReadException {
        return this.contextDefReader.read(id);
    }

    @Override
    public List<ContextDefinition> readContextDefinitions(String... propIds) throws KnowledgeSourceReadException {
        return this.contextDefReader.read(propIds);
    }

    @Override
    public boolean hasAbstractionDefinition(String id) throws KnowledgeSourceReadException {
        return this.abstractionDefReader.has(id);
    }

    @Override
    public boolean hasContextDefinition(String id) throws KnowledgeSourceReadException {
        return this.contextDefReader.has(id);
    }

    @Override
    public ValueSet readValueSet(String id) throws KnowledgeSourceReadException {
        ValueSet result = null;
        synchronized (this.notFoundValueSetRequests) {
            if (!this.notFoundValueSetRequests.containsKey(id)) {
                if (propositionDefinitionCache != null) {
                    synchronized (propositionDefinitionCache) {
                        result = propositionDefinitionCache.getValueSet(id);
                        if (result == null) {
                            initializeIfNeeded("reading the value set {0}", id);
                            if (getBackends() != null) {
                                for (KnowledgeSourceBackend backend : getBackends()) {
                                    result = backend.readValueSet(id);
                                    if (result != null) {
                                        try {
                                            this.propositionDefinitionCache.addValueSet(result);
                                        } catch (InvalidValueSetDefinitionException ex) {
                                            ProtempaUtil.logger().log(Level.SEVERE, "Error adding value set definition to cache", ex);
                                            throw new AssertionError("Error adding value set definition to cache: " + ex.getMessage());
                                        }
                                        return result;
                                    }
                                }
                                this.notFoundValueSetRequests.put(id, null);
                            }
                        }
                    }
                }
            }
        }
        return result;
    }

    @Override
    public boolean hasValueSet(String id) throws KnowledgeSourceReadException {
        boolean result = false;
        synchronized (this.notFoundValueSetRequests) {
            if (!this.notFoundValueSetRequests.containsKey(id)) {
                if (propositionDefinitionCache != null) {
                    synchronized (propositionDefinitionCache) {
                        result = propositionDefinitionCache.getValueSet(id) != null;
                    }
                }
                if (!result) {
                    initializeIfNeeded("reading the value set {0}", id);
                    for (KnowledgeSourceBackend backend : getBackends()) {
                        result = backend.readValueSet(id) != null;
                        if (result) {
                            return result;
                        }
                    }
                    this.notFoundValueSetRequests.put(id, null);
                }
            }
        }
        return result;
    }

    @Override
    public void close() throws SourceCloseException {
        clear();
        this.propositionDefinitionCache = null;
        super.close();
    }

    @Override
    public void clear() {
        if (this.propositionDefinitionCache != null) {
            synchronized (this.propositionDefinitionCache) {
                this.propositionDefinitionCache.clear();
            }
            synchronized (this.inverseIsACache) {
                this.inverseIsACache.clear();
            }
            synchronized (this.abstractedFromCache) {
                this.abstractedFromCache.clear();
            }
            synchronized (this.subContextsCache) {
                this.subContextsCache.clear();
            }
            synchronized (this.inducedByCache) {
                this.inducedByCache.clear();
            }
            synchronized (this.abstractionDefReader) {
                this.notFoundAbstractionDefinitionRequests.clear();
            }
            synchronized (this.notFoundValueSetRequests) {
                this.notFoundValueSetRequests.clear();
            }
            synchronized (this.propDefReader) {
                this.notFoundPropositionDefinitionRequests.clear();
            }
            this.inDataSourceGetter.clear();
            this.collectSubtreeGetter.clear();
        }
    }

    @Override
    public Set<String> collectPropIdDescendantsUsingAllNarrower(boolean inDataSourceOnly, String... propIds)
            throws KnowledgeSourceReadException {
        initializeIfNeeded(
                "Getting proposition ids for {0} with inDataSource set to true",
                StringUtils.join(propIds, ","));
        ProtempaUtil.checkArrayForNullElement(propIds, "propIds");
        return this.inDataSourceGetter.subtreePropIds(inDataSourceOnly, propIds);
    }

    @Override
    public Set<PropositionDefinition> collectPropDefDescendantsUsingAllNarrower(
            boolean inDataSourceOnly, String... propIds) throws KnowledgeSourceReadException {
        initializeIfNeeded(
                "Getting proposition definitions for {0} with inDataSource set to true",
                StringUtils.join(propIds, ","));
        ProtempaUtil.checkArrayForNullElement(propIds, "propIds");
        return this.inDataSourceGetter.subtreePropDefs(inDataSourceOnly, propIds);
    }

    @Override
    public Set<String> collectPropIdDescendantsUsingInverseIsA(String... propIds) throws KnowledgeSourceReadException {
        initializeIfNeeded(
                "Getting proposition ids for {0}",
                StringUtils.join(propIds, ","));
        ProtempaUtil.checkArrayForNullElement(propIds, "propIds");
        return this.collectSubtreeGetter.subtreePropIds(false, propIds);
    }

    @Override
    public Set<PropositionDefinition> collectPropDefDescendantsUsingInverseIsA(String... propIds) throws KnowledgeSourceReadException {
        initializeIfNeeded(
                "Getting proposition definitions for {0}",
                StringUtils.join(propIds, ","));
        ProtempaUtil.checkArrayForNullElement(propIds, "propIds");
        return this.collectSubtreeGetter.subtreePropDefs(false, propIds);
    }

    /**
     * Gets the mappings from term IDs to proposition IDs for each backend.
     *
     * @return a {@link Map} of {@link String}s to a {@link List} of
     * <code>String</code>s, with the keys being {@link Term} IDs and the values
     * being lists of {@link PropositionDefinition} IDs.
     */
    @Override
    public List<String> getPropositionDefinitionsByTerm(
            And<TermSubsumption> termSubsumptionClause)
            throws KnowledgeSourceReadException {
        List<String> result = new ArrayList<>();
        initializeIfNeeded("getting proposition definitions by term");
        for (KnowledgeSourceBackend backend : getBackends()) {
            result.addAll(backend.getPropositionsByTermSubsumption(termSubsumptionClause));
        }

        return result;
    }

    @Override
    public void backendUpdated(KnowledgeSourceBackendUpdatedEvent event) {
        clear();
        fireKnowledgeSourceUpdated();
    }

    /**
     * Notifies registered listeners that the knowledge source has been updated.
     *
     * @see KnowledgeSourceUpdatedEvent
     * @see SourceListener
     */
    private void fireKnowledgeSourceUpdated() {
        fireSourceUpdated(new KnowledgeSourceUpdatedEvent(this));
    }

    /**
     * Gets the proposition Definitions from each backend that match the
     * searchKey
     *
     * @param searchKey
     * @return
     * @throws KnowledgeSourceReadException
     */
    @Override
    public List<PropositionDefinition> getMatchingPropositionDefinitions(String searchKey)
            throws KnowledgeSourceReadException {
        ProtempaUtil.logger().log(Level.INFO,
                "Searching knowledge source For search string {0}", searchKey);
        List<PropositionDefinition> searchResultsPropDef = new ArrayList<>();
        Set<String> searchResults = getSearchResultsFromBackend(searchKey);
        for (String result : searchResults) {
            searchResultsPropDef.add(readPropositionDefinition(result));
        }
        return searchResultsPropDef;
    }

    private Set<String> getSearchResultsFromBackend(String searchKey)
            throws KnowledgeSourceReadException {
        Set<String> results = new HashSet<>();
        for (KnowledgeSourceBackend backend : getBackends()) {
            results.addAll(backend.getKnowledgeSourceSearchResults(searchKey));
        }
        return results;
    }
}
