/*
 * Hibernate Search, full-text search for your domain model
 *
 * License: GNU Lesser General Public License (LGPL), version 2.1 or later
 * See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
 */
package org.hibernate.search.indexes.serialization.avro.impl;

import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;

import org.apache.avro.Protocol;
import org.hibernate.search.indexes.serialization.avro.logging.impl.Log;
import org.hibernate.search.util.impl.Closeables;
import org.hibernate.search.util.impl.StreamHelper;
import org.hibernate.search.util.logging.impl.LoggerFactory;

/**
 * Helper to build an Avro Protocol version 1.0 from all our resource
 * schemas.
 */
class ProtocolBuilderV1_0 {

	private static final String AVRO_SCHEMA_FILE_SUFFIX = ".avro";
	private static final String AVRO_PROTOCOL_FILE_SUFFIX = ".avpr";
	private static String V1_0_PATH = "org/hibernate/search/remote/codex/avro/v1_0/";
	private static final Log log = LoggerFactory.make( Log.class );

	private final Map<String, String> schemas = new HashMap<String, String>();

	/**
	 * @return an Avro Protocol at version 1.0
	 */
	Protocol build() {
		parseSchema( "attribute/TokenTrackingAttribute" );
		parseSchema( "attribute/CharTermAttribute" );
		parseSchema( "attribute/PayloadAttribute" );
		parseSchema( "attribute/KeywordAttribute" );
		parseSchema( "attribute/PositionIncrementAttribute" );
		parseSchema( "attribute/FlagsAttribute" );
		parseSchema( "attribute/TypeAttribute" );
		parseSchema( "attribute/OffsetAttribute" );
		parseSchema( "field/TermVector" );
		parseSchema( "field/Index" );
		parseSchema( "field/Store" );
		parseSchema( "field/TokenStreamField" );
		parseSchema( "field/ReaderField" );
		parseSchema( "field/StringField" );
		parseSchema( "field/BinaryField" );
		parseSchema( "field/NumericIntField" );
		parseSchema( "field/NumericLongField" );
		parseSchema( "field/NumericFloatField" );
		parseSchema( "field/NumericDoubleField" );
		parseSchema( "field/CustomFieldable" );
		parseSchema( "Document" );
		parseSchema( "operation/Id" );
		parseSchema( "operation/OptimizeAll" );
		parseSchema( "operation/PurgeAll" );
		parseSchema( "operation/Delete" );
		parseSchema( "operation/Add" );
		parseSchema( "operation/Update" );
		parseSchema( "Message" );
		return parseProtocol( "Works" );
	}

	protected final void parseSchema(String filename) {
		String fullFileName = getResourceBasePath() + filename + AVRO_SCHEMA_FILE_SUFFIX;
		String schema = avroResourceAsString( fullFileName );
		schemas.put( filename, schema );
	}

	protected String getResourceBasePath() {
		return V1_0_PATH;
	}

	protected final Protocol parseProtocol(String name) {
		String fullFileName = getResourceBasePath() + name + AVRO_PROTOCOL_FILE_SUFFIX;
		String protocolSkeleton = avroResourceAsString( fullFileName );
		String protocol = inlineSchemas( protocolSkeleton );
		return Protocol.parse( protocol );
	}

	private String inlineSchemas(String protocolSkeleton) {
		String result = protocolSkeleton;
		for ( Map.Entry<String, String> entry : schemas.entrySet() ) {
			result = replace(
					result, "`" + entry.getKey() + "`",
					entry.getValue()
			);
		}
		return result;
	}

	private static String replace(String str, String pattern, String replace) {
		int s = 0;
		int e;
		StringBuilder result = new StringBuilder();
		while ( ( e = str.indexOf( pattern, s ) ) >= 0 ) {
			result.append( str.substring( s, e ) );
			result.append( replace );
			s = e + pattern.length();
		}
		result.append( str.substring( s ) );
		return result.toString();
	}

	private static String avroResourceAsString(String resourceName) {
		// using class loader of AvroSerializationProvider, because we load resources included in the same artifact.
		InputStream inputStream = AvroSerializationProvider.class.getClassLoader().getResourceAsStream( resourceName );
		if ( inputStream == null ) {
			throw log.unableToLoadAvroSchema( resourceName );
		}

		String resource;
		try {
			resource = StreamHelper.readInputStream( inputStream );
		}
		catch (IOException e) {
			throw log.unableToLoadResource( resourceName );
		}
		finally {
			Closeables.closeQuietly( inputStream );
		}
		return resource;
	}

}
