package com.swip.core;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.text.MessageFormat;
import java.util.Map;
import java.util.Objects;
import java.util.Properties;
import java.util.UUID;
import java.util.stream.Collectors;

import org.apache.commons.io.FilenameUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.swip.api.lang.Element;
import com.swip.api.lang.OperationFailedException;
import com.swip.api.lang.environment.Environment;
import com.swip.core.environment.PropertiesEnvironment;

/**
 * Implementation of the {@link Element} API as an extension of the the standard
 * Java properties to retain a reference to a file and to wrap boilerplate code
 * to read and store it.
 *
 * @author Otavio Biasutti
 */
public abstract class PropertiesElement extends Properties implements Element {

	public static final String ATTRIBUTE_NAME = "name";

	public static final String ATTRIBUTE_DESCRIPTION = "description";

	public static final String ATTRIBUTE_UUID = "uuid";

	public static final String ATTRIBUTE_LABEL = "label";

	public static final String ATTRIBUTE_CATEGORY = "category";

	protected static final String ELEMENT_PREFIX = "element.prefix";

	protected static final String KEY_SEPARATOR = ".";

	protected static final String FILENAME_SEPARATOR = "-";

	private static final long serialVersionUID = 1L;

	private static final Logger LOGGER = LoggerFactory.getLogger(PropertiesElement.class);

	private String source;

	private final String prefix;

	public PropertiesElement(final String prefix, final Path source) {
		super();

		this.prefix = prefix;
		this.source = source.toString();

		if (Files.exists(source)) {

			this.load();
		}
	}

	public PropertiesElement(final String prefix, final String category, final String label) {
		super();

		this.prefix = prefix;
		this.setProperty(ELEMENT_PREFIX, prefix);
		this.setProperty(this.buildKey(ATTRIBUTE_UUID), UUID.randomUUID().toString());
		this.setProperty(this.buildKey(ATTRIBUTE_CATEGORY), category);
		this.setProperty(this.buildKey(ATTRIBUTE_LABEL), label);
	}

	@Override
	public synchronized PropertiesElement clone() {

		return (PropertiesElement) super.clone();
	}

	@Override
	public String getAttributeValue(final String key) {

		return this.getProperty(this.buildKey(key));
	}

	@Override
	public Map<String, String> toKeyValueMap() {

		return this.entrySet().stream()
				.collect(Collectors.toMap(e -> Objects.toString(e.getKey()), e -> Objects.toString(e.getValue())));
	}

	@Override
	public String getCategory() {

		return this.getProperty(this.buildKey(ATTRIBUTE_CATEGORY));
	}

	@Override
	public String getDescription() {

		return this.getProperty(this.buildKey(ATTRIBUTE_DESCRIPTION));
	}

	@Override
	public String getLabel() {

		return this.getProperty(this.buildKey(ATTRIBUTE_LABEL));
	}

	@Override
	public String getName() {

		return this.getProperty(this.buildKey(ATTRIBUTE_NAME));
	}

	public Path getSource() {

		return Paths.get(this.source);
	}

	@Override
	public String getUuid() {

		return this.getProperty(this.buildKey(ATTRIBUTE_UUID));
	}

	@Override
	public boolean isValid() {

		return Element.super.isValid() && Objects.equals(this.getProperty(ELEMENT_PREFIX), this.prefix);
	}

	public void load() {

		try (InputStream inputStream = Files.newInputStream(this.getSource(), StandardOpenOption.READ)) {

			this.load(inputStream);

		} catch (final IOException e) {

			LOGGER.error("Error load properties file [" + this.getSource() + "]", e);
		}
	}

	@Override
	public void setAttributeValue(final String key, final String value) {

		this.setProperty(this.buildKey(key), value);
	}

	public void store(final Path parentFolder) {

		if (parentFolder == null || Files.isRegularFile(parentFolder)) {

			throw new IllegalArgumentException("The given path [" + parentFolder + "] is not a folder.");
		}

		if (!Files.exists(parentFolder)) {

			try {

				Files.createDirectories(parentFolder);

			} catch (final IOException e) {

				throw new OperationFailedException(
						"Error creating parent properties file folder [" + parentFolder + "]", e);
			}
		}

		this.source = parentFolder.resolve(this.buildFilename()).toString();

		if (!Files.exists(this.getSource())) {

			try (OutputStream newOutputStream = Files.newOutputStream(this.getSource(), StandardOpenOption.CREATE,
					StandardOpenOption.WRITE)) {

				this.store(newOutputStream, null);

			} catch (final IOException e) {

				throw new OperationFailedException("Error writing properties file [" + this.getSource() + "]", e);
			}
		}
	}

	public Environment toEnvironment() {

		return new PropertiesEnvironment(this.getKeyPrefix(), this.getCategory(), this.getLabel());
	}

	@Override
	public synchronized String toString() {

		final StringBuilder builder = new StringBuilder();

		builder.append(this.getKeyPrefix());
		builder.append(" ");
		builder.append(this.getCategory());
		builder.append(":");
		builder.append(this.getLabel());

		return builder.toString();
	}

	/**
	 * @return the filename from the element's properties
	 */
	protected String buildFilename() {

		final String category = this.getCategory();
		final String label = this.getLabel();

		return MessageFormat.format("{0}{1}{2}.properties", category, FILENAME_SEPARATOR, label);
	}

	/**
	 * @return the full key to fetch its value in the properties map
	 */
	protected final String buildKey(final String key) {

		return MessageFormat.format("{0}{1}{2}", this.getKeyPrefix(), KEY_SEPARATOR, key);
	}

	protected String getKeyPrefix() {

		return this.prefix;
	}

	/**
	 * Writes the fields derived from the filename.
	 */
	protected final void parseFilename() {

		final String baseName = FilenameUtils.getBaseName(this.getSource().toString());

		final String[] props = baseName.split(FILENAME_SEPARATOR);

		this.setProperty(this.buildKey(ATTRIBUTE_CATEGORY), props[0]);
		this.setProperty(this.buildKey(ATTRIBUTE_LABEL), props[1]);
	}
}
