/*
 * 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.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.Set;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;

import org.apache.brooklyn.api.catalog.CatalogItem;
import org.apache.brooklyn.api.mgmt.rebind.RebindSupport;
import org.apache.brooklyn.api.mgmt.rebind.mementos.CatalogItemMemento;
import org.apache.brooklyn.config.ConfigKey;
import org.apache.brooklyn.core.mgmt.rebind.BasicCatalogItemRebindSupport;
import org.apache.brooklyn.core.objs.AbstractBrooklynObject;
import org.apache.brooklyn.core.relations.EmptyRelationSupport;
import org.apache.brooklyn.util.collections.MutableList;
import org.apache.brooklyn.util.core.flags.FlagUtils;
import org.apache.brooklyn.util.core.flags.SetFromFlag;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.Objects;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Sets;

public abstract class CatalogItemDtoAbstract<T, SpecT> extends AbstractBrooklynObject implements CatalogItem<T, SpecT> {

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

    private @SetFromFlag String symbolicName;
    private @SetFromFlag String version = BasicBrooklynCatalog.NO_VERSION;

    private @SetFromFlag String displayName;
    private @SetFromFlag String description;
    private @SetFromFlag String iconUrl;

    private @SetFromFlag String javaType;
    /**@deprecated since 0.7.0, left for deserialization backwards compatibility (including xml based catalog format) */
    private @Deprecated @SetFromFlag String type;
    private @SetFromFlag String planYaml;

    private @SetFromFlag Collection<CatalogBundle> libraries;
    private @SetFromFlag Set<Object> tags = Sets.newLinkedHashSet();
    private @SetFromFlag boolean deprecated;
    private @SetFromFlag boolean disabled;

    /**
     * @throws UnsupportedOperationException; Config not supported for catalog item. See {@link #getPlanYaml()}.
     */
    @Override
    public ConfigurationSupportInternal config() {
        throw new UnsupportedOperationException();
    }
    
    /**
     * @throws UnsupportedOperationException; subscriptions are not supported for catalog items
     */
    @Override
    public SubscriptionSupportInternal subscriptions() {
        throw new UnsupportedOperationException();
    }
    
    @Override
    public <U> U getConfig(ConfigKey<U> key) {
        return config().get(key);
    }
    
    @Override
    public <U> U setConfig(ConfigKey<U> key, U val) {
        return config().set(key, val);
    }
    
    @Override
    public String getId() {
        return getCatalogItemId();
    }

    @Override
    public String getCatalogItemId() {
        return CatalogUtils.getVersionedId(getSymbolicName(), getVersion());
    }

    @Override
    public String getJavaType() {
        if (javaType != null) return javaType;
        return type;
    }

    @Override
    @Deprecated
    public String getName() {
        return getDisplayName();
    }

    @Override
    @Deprecated
    public String getRegisteredTypeName() {
        return getSymbolicName();
    }

    @Override
    public String getDisplayName() {
        return displayName;
    }

    @Override
    public String getDescription() {
        return description;
    }

    @Override
    public String getIconUrl() {
        return iconUrl;
    }

    @Override
    public String getSymbolicName() {
        if (symbolicName != null) return symbolicName;
        return getJavaType();
    }

    @Override
    public String getVersion() {
        // The property is set to NO_VERSION when the object is initialized so it's not supposed to be null ever.
        // But xstream doesn't call constructors when reading from the catalog.xml file which results in null value
        // for the version property. That's why we have to fix it in the getter.
        if (version != null) {
            return version;
        } else {
            return BasicBrooklynCatalog.NO_VERSION;
        }
    }

    @Override
    public boolean isDeprecated() {
        return deprecated;
    }

    @Override
    public void setDeprecated(boolean deprecated) {
        this.deprecated = deprecated;
    }

    @Override
    public boolean isDisabled() {
        return disabled;
    }

    @Override
    public void setDisabled(boolean disabled) {
        this.disabled = disabled;
    }

    @Nonnull
    @Override
    public Collection<CatalogBundle> getLibraries() {
        if (libraries != null) {
            return ImmutableList.copyOf(libraries);
        } else {
            return Collections.emptyList();
        }
    }

    @Nullable @Override
    public String getPlanYaml() {
        return planYaml;
    }

    @Override
    public int hashCode() {
        return Objects.hashCode(symbolicName, planYaml, javaType, nullIfEmpty(libraries), version, getCatalogItemId());
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null) return false;
        if (getClass() != obj.getClass()) return false;
        CatalogItemDtoAbstract<?,?> other = (CatalogItemDtoAbstract<?,?>) obj;
        if (!Objects.equal(symbolicName, other.symbolicName)) return false;
        if (!Objects.equal(planYaml, other.planYaml)) return false;
        if (!Objects.equal(javaType, other.javaType)) return false;
        if (!Objects.equal(nullIfEmpty(libraries), nullIfEmpty(other.libraries))) return false;
        if (!Objects.equal(getCatalogItemId(), other.getCatalogItemId())) return false;
        if (!Objects.equal(version, other.version)) return false;
        if (!Objects.equal(deprecated, other.deprecated)) return false;
        if (!Objects.equal(description, other.description)) return false;
        if (!Objects.equal(displayName, other.displayName)) return false;
        if (!Objects.equal(iconUrl, other.iconUrl)) return false;
        if (!Objects.equal(tags, other.tags)) return false;
        // 'type' not checked, because deprecated, 
        // and in future we might want to allow it to be removed/blanked in some impls without affecting equality
        // (in most cases it is the same as symbolicName so doesn't matter)
        return true;
    }

    private static <T> Collection<T> nullIfEmpty(Collection<T> coll) {
        if (coll==null || coll.isEmpty()) return null;
        return coll;
    }

    @Override
    public String toString() {
        return getClass().getSimpleName()+"["+getId()+"/"+getDisplayName()+"]";
    }

    @Override
    public abstract Class<SpecT> getSpecType();

    transient CatalogXmlSerializer serializer;

    @Override
    public String toXmlString() {
        if (serializer==null) loadSerializer();
        return serializer.toString(this);
    }

    private synchronized void loadSerializer() {
        if (serializer == null) {
            serializer = new CatalogXmlSerializer();
        }
    }

    @Override
    public RebindSupport<CatalogItemMemento> getRebindSupport() {
        return new BasicCatalogItemRebindSupport(this);
    }

    /**
     * Overrides the parent so that relations are not visible.
     * @return an immutable empty relation support object; relations are not supported,
     * but we do not throw on access to enable reads in a consistent manner
     */
    @Override
    public RelationSupportInternal<CatalogItem<T,SpecT>> relations() {
        return new EmptyRelationSupport<CatalogItem<T,SpecT>>(this);
    }

    @Override
    public void setDisplayName(String newName) {
        this.displayName = newName;
    }

    @Override
    protected CatalogItemDtoAbstract<T, SpecT> configure(Map<?, ?> flags) {
        FlagUtils.setFieldsFromFlags(flags, this);
        return this;
    }

    @Override
    public TagSupport tags() {
        return new BasicTagSupport();
    }

    /*
     * Using a custom tag support class rather than the one in AbstractBrooklynObject because
     * when XStream unmarshals a catalog item with no tags (e.g. from any catalog.xml file)
     * super.tags will be null, and any call to getTags throws a NullPointerException on the
     * synchronized (tags) statement. It can't just be initialised here because super.tags is
     * final.
     */
    private class BasicTagSupport implements TagSupport {

        private void setTagsIfNull() {
            // Possible if the class was unmarshalled by Xstream with no tags
            synchronized (CatalogItemDtoAbstract.this) {
                if (tags == null) {
                    tags = Sets.newLinkedHashSet();
                }
            }
        }

        @Nonnull
        @Override
        public Set<Object> getTags() {
            synchronized (CatalogItemDtoAbstract.this) {
                setTagsIfNull();
                return ImmutableSet.copyOf(tags);
            }
        }

        @Override
        public boolean containsTag(Object tag) {
            synchronized (CatalogItemDtoAbstract.this) {
                setTagsIfNull();
                return tags.contains(tag);
            }
        }

        @Override
        public boolean addTag(Object tag) {
            boolean result;
            synchronized (CatalogItemDtoAbstract.this) {
                setTagsIfNull();
                result = tags.add(tag);
            }
            onTagsChanged();
            return result;
        }

        @Override
        public boolean addTags(Iterable<?> newTags) {
            boolean result;
            synchronized (CatalogItemDtoAbstract.this) {
                setTagsIfNull();
                result = Iterables.addAll(tags, newTags);
            }
            onTagsChanged();
            return result;
        }

        @Override
        public boolean removeTag(Object tag) {
            boolean result;
            synchronized (CatalogItemDtoAbstract.this) {
                setTagsIfNull();
                result = tags.remove(tag);
            }
            onTagsChanged();
            return result;
        }
    }

    @Override
    @Deprecated
    public void setCatalogItemId(String id) {
        //no op, should be used by rebind code only
    }

    protected void setSymbolicName(String symbolicName) {
        this.symbolicName = symbolicName;
    }

    protected void setVersion(String version) {
        this.version = version;
    }

    protected void setDescription(String description) {
        this.description = description;
    }

    protected void setIconUrl(String iconUrl) {
        this.iconUrl = iconUrl;
    }

    protected void setJavaType(String javaType) {
        this.javaType = javaType;
        this.type = null;
    }

    protected void setPlanYaml(String planYaml) {
        this.planYaml = planYaml;
    }

    protected void setLibraries(Collection<CatalogBundle> libraries) {
        this.libraries = libraries;
    }

    protected void setTags(Set<Object> tags) {
        this.tags = tags;
    }

    protected void setSerializer(CatalogXmlSerializer serializer) {
        this.serializer = serializer;
    }

    /**
     * Parses an instance of CatalogLibrariesDto from the given List. Expects the list entries
     * to be either Strings or Maps of String -> String. Will skip items that are not.
     */
    public static Collection<CatalogBundle> parseLibraries(Collection<?> possibleLibraries) {
        Collection<CatalogBundle> dto = MutableList.of();
        for (Object object : possibleLibraries) {
            if (object instanceof Map) {
                Map<?, ?> entry = (Map<?, ?>) object;
                String name = stringValOrNull(entry, "name");
                String version = stringValOrNull(entry, "version");
                String url = stringValOrNull(entry, "url");
                dto.add(new CatalogBundleDto(name, version, url));
            } else if (object instanceof String) {
                String inlineRef = (String) object;

                final String name;
                final String version;
                final String url;

                //Infer reference type (heuristically)
                if (inlineRef.contains("/") || inlineRef.contains("\\")) {
                    //looks like an url/file path
                    name = null;
                    version = null;
                    url = inlineRef;
                } else if (CatalogUtils.looksLikeVersionedId(inlineRef)) {
                    //looks like a name+version ref
                    name = CatalogUtils.getSymbolicNameFromVersionedId(inlineRef);
                    version = CatalogUtils.getVersionFromVersionedId(inlineRef);
                    url = null;
                } else {
                    //assume it to be relative url
                    name = null;
                    version = null;
                    url = inlineRef;
                }

                dto.add(new CatalogBundleDto(name, version, url));
            } else {
                LOG.debug("Unexpected entry in libraries list neither string nor map: " + object);
            }
        }
        return dto;
    }

    private static String stringValOrNull(Map<?, ?> map, String key) {
        Object val = map.get(key);
        return val != null ? String.valueOf(val) : null;
    }
}
