package com.atlassian.stash.internal.project;

import com.atlassian.bitbucket.project.Project;
import com.atlassian.bitbucket.project.ProjectType;
import com.atlassian.bitbucket.server.ApplicationMode;
import com.atlassian.bitbucket.validation.ConstraintNature;
import com.atlassian.bitbucket.validation.groups.Create;
import com.atlassian.bitbucket.validation.groups.TrustedCreate;
import com.atlassian.bitbucket.validation.groups.TrustedUpdate;
import com.atlassian.bitbucket.validation.groups.Update;
import com.atlassian.stash.internal.ApplicationConstants;
import com.atlassian.stash.internal.Initializable;
import com.atlassian.stash.internal.validation.ProjectKeyUnique;
import com.atlassian.stash.internal.validation.ProjectNameUnique;
import com.atlassian.stash.internal.validation.UnsupportedInApplicationMode;
import org.apache.commons.lang3.StringUtils;
import org.hibernate.annotations.CacheConcurrencyStrategy;

import javax.annotation.Nonnull;
import javax.persistence.*;
import java.util.Locale;
import java.util.Objects;

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

@Cacheable
@DiscriminatorColumn(name = "project_type", discriminatorType = DiscriminatorType.INTEGER)
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@ProjectKeyUnique(groups = {Create.class, TrustedCreate.class, Update.class, TrustedUpdate.class}, payload = ConstraintNature.Conflict.class)
@ProjectNameUnique(groups = {Create.class, TrustedCreate.class, Update.class, TrustedUpdate.class}, payload = ConstraintNature.Conflict.class)
@Table(name = InternalProject.TABLE, indexes = {
        @Index(name = "idx_project_key", columnList = "namespace,project_key"),
        @Index(name = "idx_project_name", columnList = "namespace,name"),
        @Index(name = "idx_project_type", columnList = "project_type")
}, uniqueConstraints = {
        @UniqueConstraint(name = "uk_project_name", columnNames = {"namespace", "name"}),
        @UniqueConstraint(name = "uk_project_key", columnNames = {"namespace", "project_key"})
})
@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public abstract class InternalProject implements Initializable, Project {

    private static final String ID_GEN = "projectIdGenerator";
    static final String TABLE = "project";

    @Column(name = "description")
    private final String description;

    @Column(name = "id", nullable = false, unique = true)
    @Id
    @GeneratedValue(generator = ID_GEN, strategy = GenerationType.TABLE)
    @TableGenerator(name = ID_GEN, table = ApplicationConstants.ID_SEQUENCE_TABLE, pkColumnValue = TABLE, allocationSize = 20)
    private final int id;

    @Column(name = "project_key")
    private final String key;

    @Column(name = "name")
    private final String name;

    @Column(name = "namespace", unique = false)
    // '#' is a safe placeholder for null because it is not allowed by the pattern restriction on Project#getNamespace()
    // See NullPlaceHolderStringType for details
    @org.hibernate.annotations.Type(type = "com.atlassian.stash.internal.hibernate.NullPlaceholderStringType", parameters =
        @org.hibernate.annotations.Parameter(name = "placeholder", value = "#"))
    @UnsupportedInApplicationMode(applicationMode = ApplicationMode.DEFAULT)
    private final String namespace;

    @Column(name = "project_type", nullable = false, updatable = false)
    @org.hibernate.annotations.Type(type = "com.atlassian.hibernate.extras.type.GenericEnumUserType", parameters =
            @org.hibernate.annotations.Parameter(name = "enumClass", value = "com.atlassian.bitbucket.project.ProjectType"))
    private final ProjectType type;

    // for hibernate
    protected InternalProject() {
        description = null;
        id = ApplicationConstants.UNSAVED_ENTITY;
        key = null;
        name = null;
        namespace = null;
        type = ProjectType.NORMAL;
    }

    protected InternalProject(int id, String namespace, String key, String name, String description, ProjectType type) {
        this.description = description;
        this.id = id;
        this.key = StringUtils.upperCase(StringUtils.trimToNull(StringUtils.defaultString(key, name)), Locale.ROOT);
        this.namespace = namespaceOf(namespace);
        this.name = StringUtils.trimToNull(name);
        this.type = type;
    }

    @Nonnull
    public static String namespaceOf(String value) {
        return StringUtils.upperCase(StringUtils.trimToNull(value), Locale.ROOT);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o instanceof InternalProject) {
            InternalProject that = (InternalProject) o;

            return Objects.equals(getId(), that.getId());
        }
        return false;
    }

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

    @Override
    public int getId() {
        return id;
    }

    @Nonnull
    @Override
    public String getKey() {
        return key;
    }

    @Nonnull
    @Override
    public String getName() {
        return name;
    }

    public String getNamespace() {
        return namespace;
    }

    @Nonnull
    @Override
    public ProjectType getType() {
        return type;
    }

    @Override
    public int hashCode() {
        return Objects.hashCode(getId());
    }

    @Override
    public void initialize() {
        //This base implementation is empty, because InternalProject and InternalNormalProject have no entity
        //relationships (so there are no fields that need to be initialized)
    }

    @Override
    public String toString() {
        return "Project{id=" + id + ", key='" + key + "', name='" + name + "'}";
    }

    public abstract static class AbstractBuilder<B extends AbstractBuilder<B>> {

        protected int id;
        protected String namespace;

        public AbstractBuilder() {
            id = ApplicationConstants.UNSAVED_ENTITY;
        }

        public AbstractBuilder(@Nonnull Project project) {
            id = checkNotNull(project, "project").getId();
            namespace = project.getNamespace();
        }

        @Nonnull
        public B id(int value) {
            id = value;

            return self();
        }

        @Nonnull
        public B namespace(String value) {
            namespace = value;
            return self();
        }

        @Nonnull
        protected abstract B self();
    }
}

