package jp.sourceforge.asclipse.as3.internal;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import jp.sourceforge.asclipse.as3.AS3Lexer;
import jp.sourceforge.asclipse.as3.AS3Parser;
import jp.sourceforge.asclipse.as3.IAS3Context;
import jp.sourceforge.asclipse.as3.IAS3GlobalContext;
import jp.sourceforge.asclipse.as3.AS3Parser.fileContents_return;
import jp.sourceforge.asclipse.as3.ParserUtil.AS3ParserException;
import jp.sourceforge.asclipse.as3.builder.TreeBuilder;
import jp.sourceforge.asclipse.as3.element.AS3Element;
import jp.sourceforge.asclipse.as3.element.AS3Root;
import jp.sourceforge.asclipse.as3.element.AS3Type;
import jp.sourceforge.asclipse.as3.internal.element.ModifiableAS3Root;

import org.antlr.runtime.ANTLRInputStream;
import org.antlr.runtime.CommonTokenStream;
import org.antlr.runtime.RecognitionException;
import org.antlr.runtime.tree.CommonTree;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * {@link IAS3Context}の内部用の実装。
 * @author shin1ogawa
 */
@SuppressWarnings("serial")
public class DefaultAS3Context implements IAS3Context {

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

	private final List<IAS3ContextListener> listeners = new ArrayList<IAS3ContextListener>();

	private final List<AS3Root> trees = new ArrayList<AS3Root>();

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

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

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

	private final String rootFolder;


	/**
	 * Constructor.
	 * @param rootFolder
	 * @category constructor
	 */
	public DefaultAS3Context(String rootFolder) {
		this.rootFolder = rootFolder;
	}

	public void addAS3Root(AS3Root root) {
		trees.add(root);
		AS3Type typeElement = getTypeElement(root);
		if (typeElement != null) {
			String qualifiedName = typeElement.getQualifiedName();
			typeRootMap.put(qualifiedName, root);
			typeElementMap.put(qualifiedName, typeElement);
			rootTypeMap.put(root, qualifiedName);
		}
		fireAdded(root);
	}

	public void removeAS3Root(AS3Root root) {
		trees.remove(root);
		String qualifiedName = rootTypeMap.get(root);
		if (qualifiedName != null) {
			typeRootMap.remove(qualifiedName);
			typeElementMap.remove(qualifiedName);
			rootTypeMap.remove(root);
		}
		fireRemoved(root);
	}

	public void addListener(IAS3ContextListener listener) {
		listeners.add(listener);
	}

	public void removeListener(IAS3ContextListener listener) {
		listeners.remove(listener);
	}

	public String getRootFolder() {
		return rootFolder;
	}

	public List<AS3Root> getAS3Roots() {
		return Collections.unmodifiableList(trees);
	}

	public AS3Root getAS3Root(String resourceName) {
		for (AS3Root root : trees) {
			if (resourceName.equals(root.getResourceName())) {
				return root;
			}
		}
		return null;
	}

	public AS3Root parseInput(IAS3GlobalContext globalContext, InputStream input,
			String resourceName) throws AS3ParserException {
		AS3Root oldRoot = getAS3Root(resourceName);
		if (oldRoot != null) {
			this.removeAS3Root(oldRoot);
		}
		AS3Root newRoot = parse(globalContext, input, resourceName);
		addAS3Root(newRoot);
		return newRoot;
	}

	private AS3Root parse(IAS3GlobalContext globalContext, InputStream input, String resourceName)
			throws AS3ParserException {
		try {
			ANTLRInputStream antlrIs;
			antlrIs = new ANTLRInputStream(input);
			AS3Lexer lexer = new AS3Lexer(antlrIs);
			CommonTokenStream tokenStream = new CommonTokenStream(lexer);
			AS3Parser parser = new AS3Parser(tokenStream);
			parser.setBacktrackingLevel(0);
			fileContents_return fileContents = parser.fileContents();
			if (fileContents == null) {
				throw new AS3ParserException("fail to parse. resourseName=" + resourceName);
			}
			CommonTree tree = (CommonTree) fileContents.getTree();
			if (tree == null) {
				throw new AS3ParserException("fail to parse. resourseName=" + resourceName);
			}
			ModifiableAS3Root root = null;
			try {
				root = new TreeBuilder().build(globalContext, tree);
			} catch (Exception ex) {
				//
			}
			if (root == null) {
				throw new AS3ParserException("fail to build. resourseName=" + resourceName);
			}
			if (resourceName != null) {
				root.setResourceName(resourceName);
			}
			List<Exception> parseErrors = parser.getParseErrors();
			if (parseErrors != null) {
				root.setParserErrors(parseErrors);
			}
			return root;
		} catch (IOException e) {
			LOGGER.warn("parse error.", e);
			throw new AS3ParserException(e);
		} catch (RecognitionException e) {
			LOGGER.warn("parse error.", e);
			throw new AS3ParserException(e);
		}
	}

	public AS3Root parseFile(IAS3GlobalContext globalContext, File file) throws AS3ParserException,
			FileNotFoundException {
		String rootFolderName = new File(getRootFolder()).getAbsolutePath();
		String resourceName = file.getAbsolutePath().replaceFirst(rootFolderName, "");
		InputStream input = new FileInputStream(file);
		try {
			return parseInput(globalContext, input, resourceName);
		} finally {
			try {
				input.close();
			} catch (IOException e) {
				// Nothing to do.
			}
		}
	}

	protected void fireAdded(AS3Root root) {
		IAS3ContextListener[] array = listeners.toArray(new IAS3ContextListener[0]);
		for (IAS3ContextListener listener : array) {
			listener.added(root);
		}
	}

	protected void fireRemoved(AS3Root root) {
		IAS3ContextListener[] array = listeners.toArray(new IAS3ContextListener[0]);
		for (IAS3ContextListener listener : array) {
			listener.removed(root);
		}
	}

	private static AS3Type getTypeElement(AS3Element element) {
		if (AS3Type.class.isAssignableFrom(element.getClass())) {
			return (AS3Type) element;
		}
		List<AS3Element> children = element.getChildren();
		for (AS3Element child : children) {
			AS3Type firstElement = getTypeElement(child);
			if (firstElement != null) {
				return firstElement;
			}
		}
		return null;
	}

	public Collection<AS3Type> getAllTypes() {
		return typeElementMap.values();
	}

	public AS3Type getTypeElement(String qualifiedName) {
		Iterator<Entry<String, AS3Type>> iterator = typeElementMap.entrySet().iterator();
		while (iterator.hasNext()) {
			Entry<String, AS3Type> next = iterator.next();
			if (next.getKey().equals(qualifiedName)) {
				return next.getValue();
			}
		}
		return null;
	}

	public List<AS3Type> getTypesInPackage(String packageName) {
		String prefix = packageName + ".";
		List<AS3Type> types = new ArrayList<AS3Type>();
		Iterator<Entry<String, AS3Type>> iterator = typeElementMap.entrySet().iterator();
		while (iterator.hasNext()) {
			Entry<String, AS3Type> next = iterator.next();
			String key = next.getKey();
			if (key.startsWith(prefix)) {
				if (key.replaceFirst(prefix, "").indexOf('.') < 0) {
					types.add(next.getValue());
				}
			}
		}
		return types;
	}
}
