/*
 * 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.io.InputStream;
import java.io.StringReader;
import java.util.Collection;
import java.util.List;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.brooklyn.api.catalog.CatalogItem;
import org.apache.brooklyn.util.collections.MutableList;
import org.apache.brooklyn.util.collections.MutableMap;
import org.apache.brooklyn.util.core.ResourceUtils;
import org.apache.brooklyn.util.exceptions.Exceptions;
import org.apache.brooklyn.util.exceptions.PropagatedRuntimeException;
import org.apache.brooklyn.util.stream.Streams;

import com.google.common.annotations.Beta;
import com.google.common.base.Objects;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.Lists;

@Beta
public class CatalogDto {

    private static final Logger LOG = LoggerFactory.getLogger(CatalogDto.class);

    String id;
    String url;
    String contents;
    String contentsDescription;
    String name;
    String description;
    CatalogClasspathDto classpath;
    private List<CatalogItemDtoAbstract<?,?>> entries = null;
    
    // for thread-safety, any dynamic additions to this should be handled by a method 
    // in this class which does copy-on-write
    List<CatalogDto> catalogs = null;

    public static CatalogDto newDefaultLocalScanningDto(CatalogClasspathDo.CatalogScanningModes scanMode) {
        CatalogDo result = new CatalogDo(
                newNamedInstance("Local Scanned Catalog", "All annotated Brooklyn entities detected in the default classpath", "scanning-local-classpath") );
        result.setClasspathScanForEntities(scanMode);
        return result.dto;
    }

    /** @deprecated since 0.7.0 use {@link #newDtoFromXmlUrl(String)} if you must, but note the xml format itself is deprecated */
    @Deprecated
    public static CatalogDto newDtoFromUrl(String url) {
        return newDtoFromXmlUrl(url);
    }
    
    /** @deprecated since 0.7.0 the xml format is deprecated; use YAML parse routines on BasicBrooklynCatalog */
    @Deprecated
    public static CatalogDto newDtoFromXmlUrl(String url) {
        if (LOG.isDebugEnabled()) LOG.debug("Retrieving catalog from: {}", url);
        try {
            InputStream source = ResourceUtils.create().getResourceFromUrl(url);
            String contents = Streams.readFullyStringAndClose(source);
            return newDtoFromXmlContents(contents, url);
        } catch (Throwable t) {
            Exceptions.propagateIfFatal(t);
            throw new PropagatedRuntimeException("Unable to retrieve catalog from " + url + ": " + t, t);
        }
    }

    /** @deprecated since 0.7.0 the xml format is deprecated; use YAML parse routines on BasicBrooklynCatalog */
    @Deprecated
    public static CatalogDto newDtoFromXmlContents(String xmlContents, String originDescription) {
        CatalogDto result = (CatalogDto) new CatalogXmlSerializer().deserialize(new StringReader(xmlContents));
        result.contentsDescription = originDescription;

        if (LOG.isDebugEnabled()) LOG.debug("Retrieved catalog from: {}", originDescription);
        return result;
    }

    /**
     * Creates a DTO.
     * <p>
     * The way contents is treated may change; thus this (and much of catalog) should be treated as beta.
     * 
     * @param name
     * @param description
     * @param optionalContentsDescription optional description of contents; if null, we normally expect source 'contents' to be set later;
     *   if the DTO has no 'contents' (ie XML source) then a description should be supplied so we know who is populating it
     *   (e.g. manual additions); without this, warnings may be generated
     *   
     * @return a new Catalog DTO
     */
    public static CatalogDto newNamedInstance(String name, String description, String optionalContentsDescription) {
        CatalogDto result = new CatalogDto();
        result.name = name;
        result.description = description;
        if (optionalContentsDescription!=null) result.contentsDescription = optionalContentsDescription;
        return result;
    }

    /** Used when caller wishes to create an explicitly empty catalog */
    public static CatalogDto newEmptyInstance(String optionalContentsDescription) {
        CatalogDto result = new CatalogDto();
        if (optionalContentsDescription!=null) result.contentsDescription = optionalContentsDescription;
        return result;
    }

    public static CatalogDto newLinkedInstance(String url) {
        CatalogDto result = new CatalogDto();
        result.contentsDescription = url;
        result.contents = ResourceUtils.create().getResourceAsString(url);
        return result;
    }

    /** @deprecated since 0.7.0 use {@link #newDtoFromCatalogItems(Collection, String)}, supplying a description for tracking */
    @Deprecated
    public static CatalogDto newDtoFromCatalogItems(Collection<CatalogItem<?, ?>> entries) {
        return newDtoFromCatalogItems(entries, null);
    }
    
    @SuppressWarnings({ "unchecked", "rawtypes" })
    public static CatalogDto newDtoFromCatalogItems(Collection<CatalogItem<?, ?>> entries, String description) {
        CatalogDto result = new CatalogDto();
        result.contentsDescription = description;
        // Weird casts because compiler does not seem to like
        // .copyInto(Lists.<CatalogItemDtoAbstract<?, ?>>newArrayListWithExpectedSize(entries.size()));
        result.entries = (List) FluentIterable.from(entries)
                .filter(CatalogItemDtoAbstract.class)
                .copyInto(Lists.newArrayListWithExpectedSize(entries.size()));
        return result;
    }
    
    void populate() {
        if (contents==null) {
            if (url != null) {
                contents = ResourceUtils.create().getResourceAsString(url);
                contentsDescription = url;
            } else if (contentsDescription==null) {
                LOG.debug("Catalog DTO has no contents and no description; ignoring call to populate it. Description should be set to suppress this message.");
                return;
            } else {
                LOG.trace("Nothing needs doing (no contents or URL) for catalog with contents described as "+contentsDescription+".");
                return;
            }
        }
        
        CatalogDto remoteDto = newDtoFromXmlContents(contents, contentsDescription);
        try {
            copyFrom(remoteDto, true);
        } catch (Exception e) {
            Exceptions.propagate(e);
        }
    }        

    /**
     * @throws NullPointerException If source is null (and !skipNulls)
     */
    void copyFrom(CatalogDto source, boolean skipNulls) throws IllegalAccessException {
        if (source==null) {
            if (skipNulls) return;
            throw new NullPointerException("source DTO is null, when copying to "+this);
        }
        
        if (!skipNulls || source.id != null) id = source.id;
        if (!skipNulls || source.contentsDescription != null) contentsDescription = source.contentsDescription;
        if (!skipNulls || source.contents != null) contents = source.contents;
        if (!skipNulls || source.name != null) name = source.name;
        if (!skipNulls || source.description != null) description = source.description;
        if (!skipNulls || source.classpath != null) classpath = source.classpath;
        if (!skipNulls || source.entries != null) entries = source.entries;
    }

    @Override
    public String toString() {
        return Objects.toStringHelper(this)
                .omitNullValues()
                .add("name", name)
                .add("id", id)
                .add("contentsDescription", contentsDescription)
                .toString();
    }

    // temporary fix for issue where entries might not be unique
    Iterable<CatalogItemDtoAbstract<?, ?>> getUniqueEntries() {
        if (entries==null) return null;
        Map<String, CatalogItemDtoAbstract<?, ?>> result = getEntriesMap();
        return result.values();
    }

    private Map<String, CatalogItemDtoAbstract<?, ?>> getEntriesMap() {
        if (entries==null) return null;
        Map<String,CatalogItemDtoAbstract<?, ?>> result = MutableMap.of();
        for (CatalogItemDtoAbstract<?,?> entry: entries) {
            result.put(entry.getId(), entry);
        }
        return result;
    }

    void removeEntry(CatalogItemDtoAbstract<?, ?> entry) {
        if (entries!=null)
            entries.remove(entry);
    }

    void addEntry(CatalogItemDtoAbstract<?, ?> entry) {
        if (entries == null) entries = MutableList.of();
        CatalogItemDtoAbstract<?, ?> oldEntry = getEntriesMap().get(entry.getId());
        entries.add(entry);
        if (oldEntry!=null)
            removeEntry(oldEntry);
    }

}
