package main;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

/**
 * This class contains data loaded from the Common/res/help/EditorHelp.txt file.
 * 
 * This file is under version control, and can be regenerated by running the
 * main method of LudiiDocGen/main/GenerateLudiiEditorHelpFileMain.java
 *
 * @author Dennis Soemers and cambolbro
 */
public final class EditorHelpData
{
	/**
	 * There can be only one.
	 * Since this is used only by the editor and we don't want a long pause on first key press, lazy initialisation is a bad idea
	 */
	private static final EditorHelpData SINGLETON = loadData();

	//-------------------------------------------------------------------------
	
	/** Path where we expect our data to be generated */
	private static final String RESOURCE_PATH = "/help/EditorHelp.txt";
	
	/**
	 * Mapping from type-strings to strings. The key should be a fully qualified
	 * type name such as "game.Game". The map gives the class-level documentation
	 * for this type
	 */
	private final Map<String, String> typeDocStrings;
	
	/**
	 * Mapping from type-strings to lists of strings. The key should be a fully
	 * qualified type name such as "game.Game". For every constructor of this
	 * type, the list will contain a line that fully describes that constructor.
	 */
	private final Map<String, List<String>> constructorLines;
	
	/**
	 * Mapping from type-strings to lists of lists of strings. The key should be
	 * a fully qualified type name such as "game.Game". For every constructor of
	 * this type, we have a List of Strings, where each String is a documentation
	 * line for a single parameter of that constructor.
	 */
	private final Map<String, List<List<String>>> constructorParamLines;
	
	/**
	 * Mapping from type-strings to lists of lists of strings. The key should be
	 * a fully qualified type name such as "game.Game". For every constructor of
	 * this type, we have a List of Strings, where each String is an example line
	 * for that constructor.
	 */
	private final Map<String, List<List<String>>> constructorExampleLines;
	
	/**
	 * Mapping from define-names to strings. The key should be the name of a define,
	 * such as "HopCapture". The map gives the documentation string for this define.
	 */
	private final Map<String, String> defineDocStrings;
	
	/**
	 * Mapping from define-names to lists of strings. The key should be the name of
	 * a define, such as "HopCapture". The map gives a List of Strings, where every
	 * String is an example line for the define.
	 */
	private final Map<String, List<String>> defineExampleLines;
	
	/**
	 * Mapping from type-strings to lists of strings. The key should be a fully
	 * qualified type name of an enum such as "game.functions.graph.generators.basis.brick.BrickShapeType".
	 * For every constant in this enum, the list will contain a line that describes that
	 * enum constant.
	 */
	private final Map<String, List<String>> enumConstantLines;
	
	/**
	 * Mapping from Abstract/Interface type-strings to lists of strings. The key
	 * should be a fully qualified type name of an Abstract class or Interface.
	 * For every subclass of this abstract type, the will will contain a line
	 * that describes that subclass.
	 */
	private final Map<String, List<String>> subclassDocLines;
	
	/**
	 * Mapping from type-strings to strings. The key should be a fully qualified
	 * type name such as "game.Game". The map gives @remarks documentation for this
	 * type.
	 */
	private final Map<String, String> typeRemarksStrings;
	
	static boolean trace = false;
	
	//-------------------------------------------------------------------------

	
	/**
	 * Constructor. Private, should call the static loadData() method to instantiate.
	 * 
	 * @param typeDocStrings
	 * @param constructorLines
	 * @param constructorParamLines
	 * @param constructorExampleLines
	 * @param defineDocStrings
	 * @param defineExampleLines
	 * @param enumConstantLines
	 * @param subclassDocLines
	 * @param typeRemarksStrings
	 */
	private EditorHelpData
	(
		final Map<String, String> typeDocStrings,
		final Map<String, List<String>> constructorLines,
		final Map<String, List<List<String>>> constructorParamLines,
		final Map<String, List<List<String>>> constructorExampleLines,
		final Map<String, String> defineDocStrings,
		final Map<String, List<String>> defineExampleLines,
		final Map<String, List<String>> enumConstantLines,
		final Map<String, List<String>> subclassDocLines,
		final Map<String, String> typeRemarksStrings
	)
	{
		this.typeDocStrings = typeDocStrings;
		this.constructorLines = constructorLines;
		this.constructorParamLines = constructorParamLines;
		this.constructorExampleLines = constructorExampleLines;
		this.defineDocStrings = defineDocStrings;
		this.defineExampleLines = defineExampleLines;
		this.enumConstantLines = enumConstantLines;
		this.subclassDocLines = subclassDocLines;
		this.typeRemarksStrings = typeRemarksStrings;
	}

	//-------------------------------------------------------------------------
	
	/**
	 * @return the one and only EditorHelpData object
	 */
	public static EditorHelpData get()
	{
		return SINGLETON;
	}

	/**
	 * @param type Fully qualified type name
	 * @param n Which constructor do we want?
	 * @return Grammar-like line describing the format of the nth constructor
	 * 	of given type.
	 */
	public String nthConstructorLine(final String type, final int n)
	{
		return constructorLines.get(type).get(n);
	}
	
	/**
	 * @param type Fully qualified type name
	 * @param n Which constructor do we want?
	 * @return List of strings, containing one string (javadoc comment) for every
	 * 	parameter of the nth constructor of given type
	 */
	public List<String> nthConstructorParamLines(final String type, final int n)
	{
		return constructorParamLines.get(type).get(n);
	}
	
	/**
	 * @param type Fully qualified type name
	 * @param n Which constructor do we want?
	 * @return List of strings, where each string is an example for the nth
	 * 	constructor of given type
	 */
	public List<String> nthConstructorExampleLines(final String type, final int n)
	{
		return constructorExampleLines.get(type).get(n);
	}
	
	/**
	 * @param type Fully qualified type name
	 * @return The number of constructors that the given type has
	 */
	public int numConstructors(final String type)
	{
		final List<String> lines = constructorLines.get(type);
		
//		if (lines == null) 
//			System.out.println("Lines not found for "+type);
		
		return lines == null ? 0 : lines.size();
	}
	
	/**
	 * @param type Fully qualified type name (of an enum)
	 * @return List of strings, each describing a single enum constant.
	 * 	Will be null for non-enum types.
	 */
	public List<String> enumConstantLines(final String type)
	{
		return enumConstantLines.get(type);
	}
	
	/**
	 * @param type Fully qualified type name (of an Abstract class or Interface)
	 * @return List of strings, each describing a single subclass.
	 * 	Will be null for non-abstract types.
	 */
	public List<String> subclassDocLines(final String type)
	{
		return subclassDocLines.get(type);
	}
	
	/**
	 * @param type Fully qualified type name
	 * @return Documentation for the type (i.e. class-level javadoc)
	 */
	public String typeDocString(final String type)
	{
		return typeDocStrings.get(type);
	}
	
	/**
	 * @param type Fully qualified type name
	 * @return Remarks documentation for the type (i.e. @remarks in class-level javadoc)
	 */
	public String typeRemarksString(final String type)
	{
		return typeRemarksStrings.get(type);
	}
	
	/**
	 * @param defineName Name of a define
	 * @return Documentation string for the given define
	 */
	public String defineDocString(final String defineName)
	{
		return defineDocStrings.get(defineName);
	}
	
	/**
	 * @param defineName Name of a define
	 * @return List of example lines for the given define
	 */
	public List<String> defineExampleLines(final String defineName)
	{
		return defineExampleLines.get(defineName);
	}
	
	//-------------------------------------------------------------------------
	
	/**
	 * @return Loads the help data for the Ludii editor from the resource file.
	 */
	private static EditorHelpData loadData()
	{
		final Map<String, String> typeDocStrings = new HashMap<String, String>();
		final Map<String, List<String>> constructorLines = new HashMap<String, List<String>>();
		final Map<String, List<List<String>>> constructorParamLines = new HashMap<String, List<List<String>>>();
		final Map<String, List<List<String>>> constructorExampleLines = new HashMap<String, List<List<String>>>();
		final Map<String, String> defineDocStrings = new HashMap<String, String>();
		final Map<String, List<String>> defineExampleLines = new HashMap<String, List<String>>();
		final Map<String, List<String>> enumConstantLines = new HashMap<String, List<String>>();
		final Map<String, List<String>> subclassDocLines = new HashMap<String, List<String>>();
		final Map<String, String> typeRemarksStrings = new HashMap<String, String>();
		
		// In this map we'll just collect the fully qualified names of subclasses.
		// At the end we'll use those to access the javadoc of those types
		final Map<String, List<String>> subclasses = new HashMap<String, List<String>>();
		
		try (final InputStream resource = EditorHelpData.class.getResourceAsStream(RESOURCE_PATH))
		{
			if (resource == null) 
			{
				System.err.println("Cannot locate resource " + RESOURCE_PATH + " - not all functionality will be available.\nThis file can be generated by running the\n" + 
						" main method of LudiiDocGen/main/GenerateLudiiEditorHelpFileMain.java");
				return new EditorHelpData(
						typeDocStrings, constructorLines, constructorParamLines, constructorExampleLines, 
						defineDocStrings, defineExampleLines, enumConstantLines, subclassDocLines,
						typeRemarksStrings
				);
			}
			
			try 
			(
				final InputStreamReader isr = new InputStreamReader(resource, "UTF-8");
				final BufferedReader rdr = new BufferedReader(isr)
			)
			{
				String currType = null;
				int currConstructor = -1;
				String currDefine = null;

				String line;
				while ((line = rdr.readLine()) != null)
				{
					if (line.startsWith("TYPE: "))
					{
						// We're starting to read data for a new type
						currType = line.substring("TYPE: ".length());
						constructorLines.put(currType, new ArrayList<String>());
						constructorParamLines.put(currType, new ArrayList<List<String>>());
						constructorExampleLines.put(currType, new ArrayList<List<String>>());

						currConstructor = -1;
					}
					else if (line.startsWith("TYPE JAVADOC: "))
					{
						// Type-level javadoc
						typeDocStrings.put(currType, line.substring("TYPE JAVADOC: ".length()));
					}
					else if (line.equals("NEW CTOR"))
					{
						// We're starting multiple lines of text for a new constructor
						++currConstructor;

						// Instantiate new list to contain per-param docs
						constructorParamLines.get(currType).add(new ArrayList<String>());
						
						// And same for examples
						constructorExampleLines.get(currType).add(new ArrayList<String>());
					}
					else if (line.startsWith("PARAM JAVADOC: "))
					{
						// A single-param doc line
						constructorParamLines.get(currType).get(currConstructor).add(line.substring("PARAM JAVADOC: ".length()));
					}
					else if (line.startsWith("EXAMPLE: "))
					{
						// An example for this constructor
						
						// TODO: probably want to format the example code nicely BEFORE inserting in the map?
						// example code as it is right now is always a single, unformatted line
						
						constructorExampleLines.get(currType).get(currConstructor).add(line.substring("EXAMPLE: ".length()));
					}
					else if (line.startsWith("DEFINE: "))
					{
						// We're starting to read data for a new define
						currDefine = line.substring("DEFINE: ".length());
						defineExampleLines.put(currDefine, new ArrayList<String>());
					}
					else if (line.startsWith("DEFINE JAVADOC: "))
					{
						// Javadoc for a define
						defineDocStrings.put(currDefine, line.substring("DEFINE JAVADOC: ".length()));
					}
					else if (line.startsWith("DEFINE EXAMPLE: "))
					{
						// An example for current define
						defineExampleLines.get(currDefine).add(line.substring("DEFINE EXAMPLE: ".length()));
					}
					else if (line.startsWith("CONST JAVADOC: "))
					{
						// An enum const line
						if (!enumConstantLines.containsKey(currType))
							enumConstantLines.put(currType, new ArrayList<String>());
						
						enumConstantLines.get(currType).add(line.substring("CONST JAVADOC: ".length()));

//						// Split the enum constant name from its help line						
//						final String data = line.substring("CONST JAVADOC: ".length());
//						final int c = data.indexOf(':');
//						final String name = data.substring(0, c);
//						final String help = data.substring(c + 1);
//						
//						if (!enumConstantLines.containsKey(currType))
//							enumConstantLines.put(currType, new ArrayList<String>());
//						enumConstantLines.get(currType).add(name);
//
//						typeDocStrings.put(currType, help);
					}
					else if (line.startsWith("SUBCLASS: "))
					{
						// A subclass line of an abstract type
						if (!subclasses.containsKey(currType))
							subclasses.put(currType, new ArrayList<String>());
						
						subclasses.get(currType).add(line.substring("SUBCLASS: ".length()));
					}
					else if (line.startsWith("REMARKS: "))
					{
						// Type-level remarks line
						typeRemarksStrings.put(currType, line.substring("REMARKS: ".length()));
					}
					else
					{
						// A single line describing a full constructor
						constructorLines.get(currType).add(line);
					}
				}
			}
		}
		catch (final IOException e)
		{
			e.printStackTrace();
		}
		
		// Populate our map of subclass javadoc lines for abstract types
		for (final Entry<String, List<String>> entry : subclasses.entrySet())
		{
			final String abstractType = entry.getKey();
			final List<String> subclassTypes = entry.getValue();
			
			final List<String> docLines = new ArrayList<String>(subclassTypes.size());
			for (final String subclassType : subclassTypes)
			{
				String typeDoc = typeDocStrings.get(subclassType);
				if (typeDoc == null)
				{
					typeDoc = "";
					if (trace) System.err.println("WARNING: no type doc for subclass " + subclassType + " of abstract type " + abstractType + "!");
				}
				
				docLines.add("<" + subclassType + "> : " + typeDoc);
			}
			
//			System.out.println("abstractType = " + abstractType);
//			for (final String docLine : docLines)
//			{
//				System.out.println("subclass docline = " + docLine);
//			}
			
			subclassDocLines.put(abstractType, docLines);
		}
		
		return new EditorHelpData(
				typeDocStrings, constructorLines, constructorParamLines, constructorExampleLines, 
				defineDocStrings, defineExampleLines, enumConstantLines, subclassDocLines,
				typeRemarksStrings
		);
	}
	
	//-------------------------------------------------------------------------

}
