/*
 * Copyright 2015 protobuf-dynamic developers
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.github.os72.protobuf.dynamic;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;

import com.google.protobuf.DescriptorProtos.FileDescriptorProto;
import com.google.protobuf.DescriptorProtos.FileDescriptorSet;
import com.google.protobuf.Descriptors.Descriptor;
import com.google.protobuf.Descriptors.DescriptorValidationException;
import com.google.protobuf.Descriptors.EnumDescriptor;
import com.google.protobuf.Descriptors.EnumValueDescriptor;
import com.google.protobuf.Descriptors.FileDescriptor;
import com.google.protobuf.DynamicMessage;

/**
 * DynamicSchema
 */
public class DynamicSchema
{
	// --- public static ---

	/**
	 * Creates a new dynamic schema builder
	 * 
	 * @return the schema builder
	 */
	public static Builder newBuilder() {
		return new Builder();
	}

	/**
	 * Parses a serialized schema descriptor (from input stream; closes the stream)
	 * 
	 * @param schemaDescIn the descriptor input stream
	 * @return the schema object
	 * @throws DescriptorValidationException
	 * @throws IOException
	 */
	public static DynamicSchema parseFrom(InputStream schemaDescIn) throws DescriptorValidationException, IOException {
		try {
			int len;
			byte[] buf = new byte[4096];
			ByteArrayOutputStream baos = new ByteArrayOutputStream();
			while ((len = schemaDescIn.read(buf)) > 0) baos.write(buf, 0, len);
			return parseFrom(baos.toByteArray());
		}
		finally {
			schemaDescIn.close();
		}
	}

	/**
	 * Parses a serialized schema descriptor (from byte array)
	 * 
	 * @param schemaDescBuf the descriptor byte array
	 * @return the schema object
	 * @throws DescriptorValidationException
	 * @throws IOException
	 */
	public static DynamicSchema parseFrom(byte[] schemaDescBuf) throws DescriptorValidationException, IOException {
		return new DynamicSchema(FileDescriptorSet.parseFrom(schemaDescBuf));
	}

	// --- public ---

	/**
	 * Creates a new dynamic message builder for the given message type
	 * 
	 * @param msgTypeName the message type name
	 * @return the message builder (null if not found)
	 */
	public DynamicMessage.Builder newMessageBuilder(String msgTypeName) {
		Descriptor msgType = getMessageDescriptor(msgTypeName);
		if (msgType == null) return null;
		return DynamicMessage.newBuilder(msgType);
	}

	/**
	 * Gets the protobuf message descriptor for the given message type
	 * 
	 * @param msgTypeName the message type name
	 * @return the message descriptor (null if not found)
	 */
	public Descriptor getMessageDescriptor(String msgTypeName) {
		Descriptor msgType = mMsgDescriptorMapShort.get(msgTypeName);
		if (msgType == null) msgType = mMsgDescriptorMapFull.get(msgTypeName);
		return msgType;
	}

	/**
	 * Gets the enum value for the given enum type and name
	 * 
	 * @param enumTypeName the enum type name
	 * @param enumName the enum name
	 * @return the enum value descriptor (null if not found)
	 */
	public EnumValueDescriptor getEnumValue(String enumTypeName, String enumName) {
		EnumDescriptor enumType = getEnumDescriptor(enumTypeName);
		if (enumType == null) return null;
		return enumType.findValueByName(enumName);
	}

	/**
	 * Gets the enum value for the given enum type and number
	 * 
	 * @param enumTypeName the enum type name
	 * @param enumNumber the enum number
	 * @return the enum value descriptor (null if not found)
	 */
	public EnumValueDescriptor getEnumValue(String enumTypeName, int enumNumber) {
		EnumDescriptor enumType = getEnumDescriptor(enumTypeName);
		if (enumType == null) return null;
		return enumType.findValueByNumber(enumNumber);
	}

	/**
	 * Gets the protobuf enum descriptor for the given enum type
	 * 
	 * @param enumTypeName the enum type name
	 * @return the enum descriptor (null if not found)
	 */
	public EnumDescriptor getEnumDescriptor(String enumTypeName) {
		EnumDescriptor enumType = mEnumDescriptorMapShort.get(enumTypeName);
		if (enumType == null) enumType = mEnumDescriptorMapFull.get(enumTypeName);
		return enumType;
	}

	/**
	 * Returns the message types registered with the schema
	 * 
	 * @return the set of message type names
	 */
	public Set<String> getMessageTypes() {
		return new TreeSet<String>(mMsgDescriptorMapFull.keySet());
	}

	/**
	 * Returns the enum types registered with the schema
	 * 
	 * @return the set of enum type names
	 */
	public Set<String> getEnumTypes() {
		return new TreeSet<String>(mEnumDescriptorMapFull.keySet());
	}

	/**
	 * Returns the internal file descriptor set of this schema
	 * 
	 * @return the file descriptor set
	 */
	public FileDescriptorSet getFileDescriptorSet() {
		return mFileDescSet;
	}

	/**
	 * Serializes the schema
	 * 
	 * @return the serialized schema descriptor
	 */
	public byte[] toByteArray() {
		return mFileDescSet.toByteArray();
	}

	/**
	 * Returns a string representation of the schema
	 * 
	 * @return the schema string
	 */
	public String toString() {
		Set<String> msgTypes = getMessageTypes();
		Set<String> enumTypes = getEnumTypes();
		return "types: " + msgTypes + "\nenums: " + enumTypes + "\n" + mFileDescSet;
	}

	// --- private ---

	private DynamicSchema(FileDescriptorSet fileDescSet) throws DescriptorValidationException {
		mFileDescSet = fileDescSet;
		Map<String,FileDescriptor> fileDescMap = init(fileDescSet);
		
		Set<String> msgDupes = new HashSet<String>();
		Set<String> enumDupes = new HashSet<String>();
		for (FileDescriptor fileDesc : fileDescMap.values()) {
			for (Descriptor msgType : fileDesc.getMessageTypes()) addMessageType(msgType, null, msgDupes, enumDupes);			
			for (EnumDescriptor enumType : fileDesc.getEnumTypes()) addEnumType(enumType, null, enumDupes);						
		}
		
		for (String msgName : msgDupes) mMsgDescriptorMapShort.remove(msgName);
		for (String enumName : enumDupes) mEnumDescriptorMapShort.remove(enumName);
	}

	@SuppressWarnings("unchecked")
	private Map<String,FileDescriptor> init(FileDescriptorSet fileDescSet) throws DescriptorValidationException {
		// check for dupes
		Set<String> allFdProtoNames = new HashSet<String>();
		for (FileDescriptorProto fdProto : fileDescSet.getFileList()) {
			if (allFdProtoNames.contains(fdProto.getName())) throw new IllegalArgumentException("duplicate name: " + fdProto.getName());
			allFdProtoNames.add(fdProto.getName());
		}
		
		// build FileDescriptors, resolve dependencies (imports) if any
		Map<String,FileDescriptor> resolvedFileDescMap = new HashMap<String,FileDescriptor>();
		while (resolvedFileDescMap.size() < fileDescSet.getFileCount()) {
			for (FileDescriptorProto fdProto : fileDescSet.getFileList()) {
				if (resolvedFileDescMap.containsKey(fdProto.getName())) continue;
				
				List<String> dependencyList = fdProto.getDependencyList();
				List<FileDescriptor> resolvedFdList = new ArrayList<FileDescriptor>();
				for (String depName : dependencyList) {
					if (!allFdProtoNames.contains(depName)) throw new IllegalArgumentException("cannot resolve import " + depName + " in " + fdProto.getName());
					FileDescriptor fd = resolvedFileDescMap.get(depName);
					if (fd != null) resolvedFdList.add(fd);
				}
				
				if (resolvedFdList.size() == dependencyList.size()) { // dependencies resolved
					FileDescriptor[] fds = new FileDescriptor[resolvedFdList.size()];
					FileDescriptor fd = FileDescriptor.buildFrom(fdProto, resolvedFdList.toArray(fds));
					resolvedFileDescMap.put(fdProto.getName(), fd);
				}
			}
		}
		
		return resolvedFileDescMap;
	}

	private void addMessageType(Descriptor msgType, String scope, Set<String> msgDupes, Set<String> enumDupes) {
		String msgTypeNameFull = msgType.getFullName();
		String msgTypeNameShort = (scope == null ? msgType.getName() : scope + "." + msgType.getName());
		
		if (mMsgDescriptorMapFull.containsKey(msgTypeNameFull)) throw new IllegalArgumentException("duplicate name: " + msgTypeNameFull);
		if (mMsgDescriptorMapShort.containsKey(msgTypeNameShort)) msgDupes.add(msgTypeNameShort);
		
		mMsgDescriptorMapFull.put(msgTypeNameFull, msgType);
		mMsgDescriptorMapShort.put(msgTypeNameShort, msgType);
		
		for (Descriptor nestedType : msgType.getNestedTypes()) addMessageType(nestedType, msgTypeNameShort, msgDupes, enumDupes);
		for (EnumDescriptor enumType : msgType.getEnumTypes()) addEnumType(enumType, msgTypeNameShort, enumDupes);
	}

	private void addEnumType(EnumDescriptor enumType, String scope, Set<String> enumDupes) {
		String enumTypeNameFull = enumType.getFullName();
		String enumTypeNameShort = (scope == null ? enumType.getName() : scope + "." + enumType.getName());
		
		if (mEnumDescriptorMapFull.containsKey(enumTypeNameFull)) throw new IllegalArgumentException("duplicate name: " + enumTypeNameFull);
		if (mEnumDescriptorMapShort.containsKey(enumTypeNameShort)) enumDupes.add(enumTypeNameShort);
		
		mEnumDescriptorMapFull.put(enumTypeNameFull, enumType);
		mEnumDescriptorMapShort.put(enumTypeNameShort, enumType);
	}

	private FileDescriptorSet mFileDescSet;
	private Map<String,Descriptor> mMsgDescriptorMapFull = new HashMap<String,Descriptor>();
	private Map<String,Descriptor> mMsgDescriptorMapShort = new HashMap<String,Descriptor>();
	private Map<String,EnumDescriptor> mEnumDescriptorMapFull = new HashMap<String,EnumDescriptor>();
	private Map<String,EnumDescriptor> mEnumDescriptorMapShort = new HashMap<String,EnumDescriptor>();

	/**
	 * DynamicSchema.Builder
	 */
	public static class Builder
	{
		// --- public ---

		/**
		 * Builds a dynamic schema
		 * 
		 * @return the schema object
		 * @throws DescriptorValidationException
		 */
		public DynamicSchema build() throws DescriptorValidationException {
			FileDescriptorSet.Builder fileDescSetBuilder = FileDescriptorSet.newBuilder();
			fileDescSetBuilder.addFile(mFileDescProtoBuilder.build());
			fileDescSetBuilder.mergeFrom(mFileDescSetBuilder.build());
			return new DynamicSchema(fileDescSetBuilder.build());
		}

		public Builder setName(String name) {
			mFileDescProtoBuilder.setName(name);
			return this;
		}

		public Builder setPackage(String name) {
			mFileDescProtoBuilder.setPackage(name);
			return this;
		}

		public Builder addMessageDefinition(MessageDefinition msgDef) {
			mFileDescProtoBuilder.addMessageType(msgDef.getMessageType());
			return this;
		}

		public Builder addEnumDefinition(EnumDefinition enumDef) {
			mFileDescProtoBuilder.addEnumType(enumDef.getEnumType());
			return this;
		}

		public Builder addDependency(String dependency) {
			mFileDescProtoBuilder.addDependency(dependency);
			return this;
		}

		public Builder addPublicDependency(String dependency) {
			for (int i = 0; i < mFileDescProtoBuilder.getDependencyCount(); i++) {
				if (mFileDescProtoBuilder.getDependency(i).equals(dependency)) {
					mFileDescProtoBuilder.addPublicDependency(i);
					return this;
				}
			}
			mFileDescProtoBuilder.addDependency(dependency);
			mFileDescProtoBuilder.addPublicDependency(mFileDescProtoBuilder.getDependencyCount() - 1);
			return this;
		}

		public Builder addSchema(DynamicSchema schema) {
			mFileDescSetBuilder.mergeFrom(schema.mFileDescSet);
			return this;
		}

		// --- private ---
		
		private Builder() {
			mFileDescProtoBuilder = FileDescriptorProto.newBuilder();
			mFileDescSetBuilder = FileDescriptorSet.newBuilder();
		}

		private FileDescriptorProto.Builder mFileDescProtoBuilder;
		private FileDescriptorSet.Builder mFileDescSetBuilder;
	}
}