/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.brooklyn.core.catalog.internal;

import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.apache.brooklyn.api.mgmt.ManagementContext;
import org.apache.brooklyn.core.catalog.internal.CatalogClasspathDo.CatalogScanningModes;
import org.apache.brooklyn.core.mgmt.internal.ManagementContextInternal;
import org.apache.brooklyn.util.collections.MutableList;
import org.apache.brooklyn.util.exceptions.Exceptions;
import org.apache.brooklyn.util.javalang.AggregateClassLoader;
import org.apache.brooklyn.util.time.CountdownTimer;
import org.apache.brooklyn.util.time.Duration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.Objects;
import com.google.common.base.Preconditions;

public class CatalogDo {

    private static final Logger log = LoggerFactory.getLogger(CatalogDo.class);
    
    volatile boolean isLoaded = false;
    final CatalogDto dto;
    ManagementContext mgmt = null;
    CatalogDo parent = null;
    
    List<CatalogDo> childrenCatalogs = new ArrayList<CatalogDo>();
    CatalogClasspathDo classpath;
    private Map<String, CatalogItemDo<?,?>> cacheById;

    AggregateClassLoader childrenClassLoader = AggregateClassLoader.newInstanceWithNoLoaders();
    ClassLoader recursiveClassLoader;

    protected CatalogDo(CatalogDto dto) {
        this.dto = Preconditions.checkNotNull(dto);
    }
    
    public CatalogDo(ManagementContext mgmt, CatalogDto dto) {
        this(dto);
        this.mgmt = mgmt;
    }

    boolean isLoaded() {
        return isLoaded;
    }

    /** Calls {@link #load(CatalogDo)} with a null parent. */
    public CatalogDo load() {
        return load(null);
    }

    /** Calls {@link #load(ManagementContext, CatalogDo)} with the catalog's existing management context. */
    public CatalogDo load(CatalogDo parent) {
        return load(mgmt, parent);
    }

    /** Calls {@link #load(ManagementContext, CatalogDo, boolean)} failing on load errors. */
    public synchronized CatalogDo load(ManagementContext mgmt, CatalogDo parent) {
        return load(mgmt, parent, true);
    }

    /** causes all URL-based catalogs to have their manifests loaded,
     * and all scanning-based classpaths to scan the classpaths
     * (but does not load all JARs)
     */
    public synchronized CatalogDo load(ManagementContext mgmt, CatalogDo parent, boolean failOnLoadError) {
        if (isLoaded()) {
            if (mgmt!=null && !Objects.equal(mgmt, this.mgmt)) {
                throw new IllegalStateException("Cannot set mgmt "+mgmt+" on "+this+" after catalog is loaded");
            }
            log.debug("Catalog "+this+" is already loaded");
            return this;
        }
        loadThisCatalog(mgmt, parent, failOnLoadError);
        loadChildrenCatalogs(failOnLoadError);
        buildCaches();
        return this;
    }

    protected synchronized void loadThisCatalog(ManagementContext mgmt, CatalogDo parent, boolean failOnLoadError) {
        if (isLoaded()) return;
        CatalogUtils.logDebugOrTraceIfRebinding(log, "Loading catalog {} into {}", this, parent);
        if (this.parent!=null && !this.parent.equals(parent))
            log.warn("Catalog "+this+" being initialised with different parent "+parent+" when already parented by "+this.parent, new Throwable("source of reparented "+this));
        if (this.mgmt!=null && !this.mgmt.equals(mgmt))
            log.warn("Catalog "+this+" being initialised with different mgmt "+mgmt+" when already managed by "+this.mgmt, new Throwable("source of reparented "+this));
        this.parent = parent;
        this.mgmt = mgmt;
        loadCatalogClasspath();
        loadCatalogItems(failOnLoadError);
        isLoaded = true;
        synchronized (this) {
            notifyAll();
        }
    }

    private void loadCatalogClasspath() {
        try {
            classpath = new CatalogClasspathDo(this);
            classpath.load();
        } catch (Exception e) {
            Exceptions.propagateIfFatal(e);
            log.error("Unable to load catalog "+this+" (ignoring): "+e);
            log.info("Trace for failure to load "+this+": "+e, e);
        }
    }

    private void loadCatalogItems(boolean failOnLoadError) {
        Iterable<CatalogItemDtoAbstract<?, ?>> entries = dto.getUniqueEntries();
        if (entries!=null) {
            for (CatalogItemDtoAbstract<?,?> entry : entries) {
                try {
                    CatalogUtils.installLibraries(mgmt, entry.getLibraries());
                } catch (Exception e) {
                    Exceptions.propagateIfFatal(e);
                    if (failOnLoadError) {
                        Exceptions.propagateAnnotated("Loading bundles for catalog item " + entry.getCatalogItemId() + " failed", e);
                    } else {
                        log.error("Loading bundles for catalog item " + entry + " failed: " + e.getMessage(), e);
                    }
                }
            }
        }
    }

    public boolean blockIfNotLoaded(Duration timeout) throws InterruptedException {
        if (isLoaded()) return true;
        synchronized (this) {
            if (isLoaded()) return true;
            CountdownTimer timer = CountdownTimer.newInstanceStarted(timeout);
            while (!isLoaded())
                if (!timer.waitOnForExpiry(this))
                    return false;
            return true;
        }
    }
    
    protected void loadChildrenCatalogs(boolean failOnLoadError) {
        if (dto.catalogs!=null) {
            for (CatalogDto child: dto.catalogs) {
                loadCatalog(child, failOnLoadError);
            }
        }
    }
    
    CatalogDo loadCatalog(CatalogDto child, boolean failOnLoadError) {
        CatalogDo childL = new CatalogDo(child);
        childrenCatalogs.add(childL);
        childL.load(mgmt, this, failOnLoadError);
        childrenClassLoader.addFirst(childL.getRecursiveClassLoader());
        clearCache(false);
        return childL;
    }

    protected Map<String, CatalogItemDo<?,?>> getIdCache() {
        Map<String, CatalogItemDo<?,?>> cache = this.cacheById;
        if (cache==null) cache = buildCaches();
        return cache;
    }

    @SuppressWarnings({ "unchecked", "rawtypes" })
    protected synchronized Map<String, CatalogItemDo<?,?>> buildCaches() {
        if (cacheById != null) return cacheById;
        CatalogUtils.logDebugOrTraceIfRebinding(log, "Building cache for {}", this);
        if (!isLoaded()) 
            log.debug("Catalog not fully loaded when loading cache of "+this);
        
        Map<String, CatalogItemDo<?,?>> cache = new LinkedHashMap<String, CatalogItemDo<?,?>>();
        
        // build the cache; first from children catalogs, then from local entities
        // so that root and near-root takes precedence over deeper items;
        // and go through in reverse order so that things at the top of the file take precedence
        // (both in the cache and in the aggregate class loader);
        // however anything added _subsequently_ will take precedence (again in both)
        if (dto.catalogs!=null) { 
            List<CatalogDo> catalogsReversed = new ArrayList<CatalogDo>(childrenCatalogs);
            Collections.reverse(catalogsReversed);
            for (CatalogDo child: catalogsReversed) {
                cache.putAll(child.getIdCache());
            }
        }
        if (dto.getUniqueEntries()!=null) {
            List<CatalogItemDtoAbstract<?,?>> entriesReversed = MutableList.copyOf(dto.getUniqueEntries());
            Collections.reverse(entriesReversed);
            for (CatalogItemDtoAbstract<?,?> entry: entriesReversed)
                cache.put(entry.getId(), new CatalogItemDo(this, entry));
        }
        this.cacheById = cache;
        return cache;
    }
    
    protected synchronized void clearCache(boolean deep) {
        this.cacheById = null;
        if (deep) {
            for (CatalogDo child : childrenCatalogs) {
                child.clearCache(true);
            }
        }
        clearParentCache();
    }
    protected void clearParentCache() {
        if (this.parent!=null)
            this.parent.clearCache(false);
    }
    
    /**
     * Adds the given entry to the catalog, with no enrichment.
     * Callers may prefer {@link CatalogClasspathDo#addCatalogEntry(CatalogItemDtoAbstract, Class)}
     */
    public synchronized void addEntry(CatalogItemDtoAbstract<?,?> entry) {
        dto.addEntry(entry);
        
        // could do clearCache(false); but this is slightly more efficient...
        if (cacheById != null) {
            @SuppressWarnings({ "unchecked", "rawtypes" })
            CatalogItemDo<?, ?> cdo = new CatalogItemDo(this, entry);
            cacheById.put(entry.getId(), cdo);
        }        
        clearParentCache();
        
        if (mgmt != null) {
            mgmt.getRebindManager().getChangeListener().onManaged(entry);
        }
   }
    
    /**
     * Removes the given entry from the catalog.
     */
    public synchronized void deleteEntry(CatalogItemDtoAbstract<?, ?> entry) {
        dto.removeEntry(entry);
        
        // could do clearCache(false); but this is slightly more efficient...
        if (cacheById != null) {
            cacheById.remove(entry.getId());
        }
        clearParentCache();
        
        if (mgmt != null) {
            // TODO: Can the entry be in more than one catalogue? The management context has no notion of
            // catalogue hierarchy so this will effectively remove it from all catalogues.
            // (YES- we're assuming ID's are unique across all catalogues; if not, things get out of sync;
            // however see note at top of BasicBrooklynCatalog --
            // manualCatalog and OSGi is used for everything now except legacy XML trees)
            mgmt.getRebindManager().getChangeListener().onUnmanaged(entry);
        }
    }

    /** returns loaded catalog, if this has been loaded */
    CatalogDo addCatalog(CatalogDto child) {
        if (dto.catalogs == null)
            dto.catalogs = new ArrayList<CatalogDto>();
        dto.catalogs.add(child);
        if (!isLoaded())
            return null;
        return loadCatalog(child, true);
    }
    
    /** adds the given urls; filters out any nulls supplied */
    public synchronized void addToClasspath(String ...urls) {
        if (dto.classpath == null)
            dto.classpath = new CatalogClasspathDto();
        for (String url: urls) {
            if (url!=null)
                dto.classpath.addEntry(url);
        }
        if (isLoaded())
            throw new IllegalStateException("dynamic classpath entry value update not supported");
        // easy enough to add, just support unload+reload (and can also allow dynamic setScan below)
        // but more predictable if we don't; the one exception is in the manualAdditionsCatalog
        // where BasicBrooklynCatalog reaches in and updates the DTO and/or CompositeClassLoader directly, if necessary
//            for (String url: urls)
//                loadedClasspath.addEntry(url);
    }

    public synchronized void setClasspathScanForEntities(CatalogScanningModes value) {
        if (dto.classpath == null)
            dto.classpath = new CatalogClasspathDto();
        dto.classpath.scan = value;
        if (isLoaded()) 
            throw new IllegalStateException("dynamic classpath scan value update not supported");
        // easy enough to add, see above
    }

    @Override
    public String toString() {
        String size = cacheById == null ? "not yet loaded" : "size " + cacheById.size();
        return "Loaded:" + dto + "(" + size + ")";
    }

    /** classloader for only the entries in this catalog's classpath */ 
    public ClassLoader getLocalClassLoader() {
        if (classpath != null) return classpath.getLocalClassLoader();
        return null;
    }

    /** recursive classloader is the local classloader plus all children catalog's classloader */
    public ClassLoader getRecursiveClassLoader() {
        if (recursiveClassLoader == null) loadRecursiveClassLoader();
        return recursiveClassLoader;
    }
    
    protected synchronized void loadRecursiveClassLoader() {
        if (recursiveClassLoader!=null) return;
        AggregateClassLoader cl = AggregateClassLoader.newInstanceWithNoLoaders();
        cl.addFirst(childrenClassLoader);
        ClassLoader local = getLocalClassLoader();
        if (local != null) cl.addFirst(local);
        if (parent == null) {
            // we are root.  include the mgmt base classloader and/or standard class loaders 
            ClassLoader base = mgmt != null ? ((ManagementContextInternal)mgmt).getBaseClassLoader() : null;
            if (base != null) cl.addFirst(base);
            else {
                cl.addFirst(getClass().getClassLoader());
                cl.addFirst(Object.class.getClassLoader());
            }
        }
        recursiveClassLoader = cl;
    }
    
    /** the root classloader is the recursive CL from the outermost catalog
     * (which includes the base classloader from the mgmt context, if set) */
    public ClassLoader getRootClassLoader() {
        if (parent != null) return parent.getRootClassLoader();
        return getRecursiveClassLoader();
    }

}
