package com.atlassian.bitbucket.mesh.repository;

import com.atlassian.bitbucket.mesh.util.PropertiesUtils;
import com.atlassian.bitbucket.mesh.util.Version;
import org.apache.commons.lang3.StringUtils;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Objects;
import java.util.Properties;

import static java.util.Objects.requireNonNull;
import static org.apache.commons.lang3.StringUtils.defaultString;

public class DataStoreProperties {

    static final String PROPERTIES_PATH = "store.properties";

    private static final String PROP_BUILD_VERSION = "store.build";
    private static final String PROP_LAYOUT_VERSION = "store.layout";
    private static final String PROP_TYPE = "store.type";

    /**
     * The <i>newest</i> version of the application started using this data store directory.
     * <p>
     * When a newer version of the application is started, this version is updated accordingly. However, if the
     * application is downgraded, this version is <i>not</i> updated.
     */
    private final Version build;
    /**
     * The <i>newest</i> version of the data store layout applied to this data store directory.
     * <p>
     * When a version of the application which uses a newer data store layout is started, this version is updated
     * accordingly. However, if the application is downgraded version which uses an older data store layout, this
     * version is <i>not</i> updated.
     */
    private final Version layout;
    /**
     * An identifier of the type of data store. This is represented as a plain string instead of an enum to allow
     * older application version to read and detect unsupported data store types, without blowing up.
     */
    private final String type;

    public DataStoreProperties(@Nonnull Version build, @Nonnull Version layout, @Nonnull String type) {
        this.build = requireNonNull(build, "build");
        this.layout = requireNonNull(layout, "layout");
        this.type = requireNonNull(type, "type");
    }

    private DataStoreProperties(@Nonnull Properties properties) {
        requireNonNull(properties, "properties");

        build = parseVersion(properties.getProperty(PROP_BUILD_VERSION));
        layout = parseVersion(properties.getProperty(PROP_LAYOUT_VERSION));
        type = defaultString(properties.getProperty(PROP_TYPE));
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        DataStoreProperties that = (DataStoreProperties) o;
        return build.equals(that.build) &&
               layout.equals(that.layout) &&
               type.equals(that.type);
    }

    @Nonnull
    public Version getBuild() {
        return build;
    }

    @Nonnull
    public Version getLayout() {
        return layout;
    }

    @Nonnull
    public String getType() {
        return type;
    }

    @Override
    public int hashCode() {
        return Objects.hash(build, layout, type);
    }

    @Nullable
    public static DataStoreProperties load(@Nonnull Path dataStoreRoot) throws IOException {
        return PropertiesUtils.loadIfPresent(dataStoreRoot.resolve(PROPERTIES_PATH))
                .map(DataStoreProperties::new)
                .orElse(null);
    }

    public void store(@Nonnull Path dataStorePath) throws IOException {
        requireNonNull(dataStorePath, "path");

        Properties properties = new Properties();
        properties.setProperty(PROP_BUILD_VERSION, build.toString());
        properties.setProperty(PROP_LAYOUT_VERSION, layout.toString());
        properties.setProperty(PROP_TYPE, type);
        PropertiesUtils.store(properties, dataStorePath.resolve(PROPERTIES_PATH));
    }

    /**
     * Parse the provided value as a version, returning {@code 0.0.0} if no value was provided. {@code 0.0.0} will
     * also be returned if the value provided isn't valid. {@code 0.0.0} is used because it's always "less" than
     * any other version.
     *
     * @param value the value to parse
     * @return the parsed version, which may be {@code 0.0.0} if the value isn't a valid version
     */
    private static Version parseVersion(String value) {
        return StringUtils.isBlank(value) ? new Version(0) : new Version(value);
    }
}
