/*
 * Syncany, www.syncany.org
 * Copyright (C) 2011-2016 Philipp C. Heckel <philipp.heckel@gmail.com>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.syncany.chunk;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.syncany.util.StringUtil;

/**
 * A transformer combines one or many stream-transforming {@link OutputStream}s and {@link InputStream}s.
 * Implementations might provide functionality to encrypt or compress output streams, and to decrypt
 * or uncompress a corresponding  input stream.
 *
 * <p>Transformers can be chained in order to allow multiple consecutive output-/input stream
 * transformers to be applied to a stream.
 *
 * <p>A transformer can be instantiated using its implementation-specific constructor, or by calling
 * its default constructor and initializing it using the {@link #init(Map) init()} method. Depending
 * on the implementation, varying settings must be passed.
 *
 * @author Philipp C. Heckel <philipp.heckel@gmail.com>
 */
public abstract class Transformer {
	private static final Logger logger = Logger.getLogger(Transformer.class.getSimpleName());
	protected Transformer nextTransformer;

	/**
	 * Creates a new transformer object (no next transformer)
	 */
	public Transformer() {
		this(null);
	}

	/**
	 * Create a new transformer, with a nested/chained transformer that will be 
	 * be applied after this transformer.
	 * 
	 * @param nextTransformer The next transformer (to be applied after this transformer)
	 */
	public Transformer(Transformer nextTransformer) {
		this.nextTransformer = nextTransformer;
	}

	/**
	 * If a transformer is instantiated via the default constructor (e.g. via a config file),
	 * it must be initialized using this method. The settings passed to the method depend
	 * on the implementation of the transformer.
	 *
	 * @param settings Implementation-specific setting map
	 * @throws Exception If the given settings are invalid or insufficient for instantiation
	 */
	public abstract void init(Map<String, String> settings) throws Exception;

	/**
	 * Create a stream-transforming {@link OutputStream}. Depending on the implementation, the
	 * bytes written to the output stream might be encrypted, compressed, etc.
	 *
	 * @param out Original output stream which is transformed by this transformer
	 * @return Returns a transformed output stream
	 * @throws IOException If an exception occurs when instantiating or writing to the stream
	 */
	public abstract OutputStream createOutputStream(OutputStream out) throws IOException;

	/**
	 * Creates a strea-transforming {@link InputStream}. Depending on the implementation, the
	 * bytes read from the input stream are uncompressed, decrypted, etc.
	 *  
	 * @param in Original input stream which is transformed by this transformer 
	 * @return Returns a transformed input stream
	 * @throws IOException If an exception occurs when instantiating or reading from the stream
	 */
	public abstract InputStream createInputStream(InputStream in) throws IOException;

	/**
	 * An implementation of a transformer must override this method to identify the 
	 * type of transformer and/or its settings.
	 */
	@Override
	public abstract String toString();

	/**
	 * Instantiates a transformer by its name using the default constructor. After creating
	 * a new transformer, it must be initialized using the {@link #init(Map) init()} method.  
	 * 
	 * <p>The given type attribute is mapped to fully qualified class name (FQCN) of the form
	 * <tt>org.syncany.chunk.XTransformer</tt>, where <tt>X</tt> is the camel-cased type
	 * attribute.  
	 * 
	 * @param type Type/name of the transformer (corresponds to its camel case class name)
	 * @return Returns a new transformer
	 * @throws Exception If the FQCN cannot be found or the class cannot be instantiated
	 */
	public static Transformer getInstance(String type) throws Exception {
		String thisPackage = Transformer.class.getPackage().getName();
		String camelCaseName = StringUtil.toCamelCase(type);
		String fqClassName = thisPackage + "." + camelCaseName + Transformer.class.getSimpleName();

		// Try to load!
		try {
			Class<?> clazz = Class.forName(fqClassName);
			return (Transformer) clazz.newInstance();
		}
		catch (Exception ex) {
			logger.log(Level.INFO, "Could not find operation FQCN " + fqClassName, ex);
			return null;
		}
	}

	public void setNextTransformer(Transformer nextTransformer) {
		this.nextTransformer = nextTransformer;
	}
}
