package com.atlassian.stash.internal.property;

import com.atlassian.bitbucket.property.PropertyMap;
import com.atlassian.bitbucket.property.PropertySupport;
import com.atlassian.bitbucket.util.BuilderSupport;
import com.google.common.base.MoreObjects;

import javax.annotation.Nonnull;
import javax.persistence.MappedSuperclass;
import javax.persistence.Transient;
import java.util.Map;

import static com.google.common.base.Preconditions.checkNotNull;

/**
 * Super class for the JPA entities implementing {@link PropertySupport}.
 *
 * @since 3.4
 */
@MappedSuperclass
public abstract class AbstractPropertySupport implements PropertySupport {

    @Transient
    private transient PropertyMap properties;

    // empty constructor that allows properties to be set later on (see setProperties()) and
    // allows subclasses to be Serializable
    protected AbstractPropertySupport() {
    }

    protected AbstractPropertySupport(@Nonnull PropertyBuilderSupport<?> builder) {
        properties = builder.properties.build();
    }

    @Nonnull
    @Override
    public PropertyMap getProperties() {
        // properties is transient, can be null after deserialization
        return MoreObjects.firstNonNull(properties, PropertyMap.EMPTY);
    }

    /**
     * Allows write-once mutability of the properties map. We don't actually enforce that this method is called at most
     * once because the interactions with Hibernate sessions can sometimes lead to the same entity being 'enriched'
     * multiple times. This method is part of the internal API, so from the perspective of plugins, the entity is still
     * considered immutable.
     *
     * @param properties the properties to set
     * @throws NullPointerException if the provided {@code properties} are {@code null}
     */
    public void setProperties(@Nonnull PropertyMap properties) {
        this.properties = checkNotNull(properties, "properties");
    }

    /**
     * Allows checking whether the properties have been set - that is, a non-{@code null}, non-empty map.
     *
     * @return {@code true} if the properties have been set
     */
    protected boolean hasProperties() {
        return this.properties != null && !properties.isEmpty();
    }

    protected abstract static class PropertyBuilderSupport<B extends PropertyBuilderSupport<B>>
            extends BuilderSupport {

        private final PropertyMap.Builder properties = new PropertyMap.Builder();

        protected PropertyBuilderSupport() {
        }

        protected PropertyBuilderSupport(@Nonnull PropertySupport propertyHolder) {
            properties.properties(propertyHolder.getProperties());
        }

        @Nonnull
        public B property(@Nonnull String key, @Nonnull Object value) {
            properties.property(key, value);

            return self();
        }

        @Nonnull
        public B properties(@Nonnull Map<String, Object> other) {
            properties.properties(other);

            return self();
        }

        @Nonnull
        protected abstract B self();
    }
}
