/*
	Copyright (c) 2009-2011 Olivier Chafik, All Rights Reserved
	
	This file is part of JNAerator (http://jnaerator.googlecode.com/).
	
	JNAerator is free software: you can redistribute it and/or modify
	it under the terms of the GNU Lesser General Public License as published by
	the Free Software Foundation, either version 3 of the License, or
	(at your option) any later version.
	
	JNAerator 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 Lesser General Public License for more details.
	
	You should have received a copy of the GNU Lesser General Public License
	along with JNAerator.  If not, see <http://www.gnu.org/licenses/>.
*/
package com.ochafik.lang.jnaerator;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringReader;

import javax.xml.xpath.XPathExpressionException;

import org.antlr.runtime.ANTLRReaderStream;
import org.antlr.runtime.CommonTokenStream;
import org.antlr.runtime.RecognitionException;
import org.w3c.dom.Document;
import org.w3c.dom.Node;

import com.ochafik.lang.jnaerator.parser.Arg;
import com.ochafik.lang.jnaerator.parser.Declarator;
import com.ochafik.lang.jnaerator.parser.Function;
import com.ochafik.lang.jnaerator.parser.Modifier;
import com.ochafik.lang.jnaerator.parser.ModifierType;
import com.ochafik.lang.jnaerator.parser.ObjCDemanglingLexer;
import com.ochafik.lang.jnaerator.parser.ObjCDemanglingParser;
import com.ochafik.lang.jnaerator.parser.ObjCppParser;
import com.ochafik.lang.jnaerator.parser.SourceFile;
import com.ochafik.lang.jnaerator.parser.StoredDeclarations;
import com.ochafik.lang.jnaerator.parser.Struct;
import com.ochafik.lang.jnaerator.parser.TaggedTypeRefDeclaration;
import com.ochafik.lang.jnaerator.parser.TypeRef;
import com.ochafik.lang.jnaerator.parser.VariablesDeclaration;
import com.ochafik.lang.jnaerator.parser.Declarator.DirectDeclarator;
import com.ochafik.lang.jnaerator.parser.Function.Type;
import com.ochafik.lang.jnaerator.parser.TypeRef.SimpleTypeRef;
import com.ochafik.xml.XMLUtils;
import com.ochafik.xml.XPathUtils;

import static com.ochafik.lang.jnaerator.parser.ElementsHelper.*;

public class BridgeSupportParser {
	final Result result;
	SourceFiles sourceFiles;
	public BridgeSupportParser(Result result, SourceFiles sourceFiles) {
		this.result = result;
		this.sourceFiles = sourceFiles;
	}

	public static void main(String[] args) {
		try {
			JNAeratorConfig config = new JNAeratorConfig();
			config.verbose = true;
			Result result = new Result(config, null, null);
			SourceFiles sourceFiles = new SourceFiles();
			File file = new File("/System/Library/Frameworks/Foundation.framework/Resources/BridgeSupport/FoundationFull.bridgesupport");
			new BridgeSupportParser(result, sourceFiles).parseBridgeSupportFile(file);
		} catch (Exception ex) {
			ex.printStackTrace();
		}
	}
	public void parseBridgeSupportFiles() throws FileNotFoundException {
		SourceFiles sfs = new SourceFiles();
		try {
			for (File bsf : result.config.bridgeSupportFiles) {
				try {
					sfs.add(parseBridgeSupportFile(bsf));
				} catch (Throwable e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
		} finally {
			if (result.config.bridgesupportOutFile != null)
			{
				if (result.config.verbose)
					System.out.println("Writing bridgesupport definitions to " + result.config.bridgesupportOutFile.getAbsolutePath());
				PrintWriter pw = new PrintWriter(result.config.bridgesupportOutFile);
				pw.println(sfs);
				pw.close();
			}
			for (SourceFile sf : sfs.getSourceFiles())
				sourceFiles.add(sf);
		}
	}

	private SourceFile parseBridgeSupportFile(File bsf) throws Exception {

		String framework = bsf.getName();
		if (framework.toLowerCase().endsWith(".bridgesupport"))
			framework = framework.substring(0, framework.length() - ".bridgesupport".length());
		
		Document xml = XMLUtils.readXML(bsf);
		
		String sourceFilePath = bsf.toString();
		Node signatures = XMLUtils.getFirstNamedNode(xml, "signatures");
		if (signatures == null)
			return null;
		
		SourceFile sf = new SourceFile();
		sf.setElementFile(sourceFilePath);

		parseStructs(signatures, sf);
		parseEnums(signatures, sf);
		parseConstants(signatures, sf);
		parseClasses(framework, signatures, sf);
		parseStringConstants(framework, signatures, sf);
		parseFunctions(framework, signatures, sf);
		
		return sf;
	}

	private void parseEnums(Node signatures, SourceFile sf) {
		for (Node en : XMLUtils.getChildrenByName(signatures, "enum")) {
			String name = XMLUtils.getAttribute(en, "name");
			String value = XMLUtils.getAttribute(en, "value");
			if (value != null) {
				try {
					try {
						int i = value.matches(".*?\\.0+$") ? value.lastIndexOf('.') : -1;
						String v = i > 0 ? value.substring(0, i) : value; 
						sf.addDeclaration(new VariablesDeclaration(typeRef("int").addModifiers(ModifierType.Const), new DirectDeclarator(name, expr(Integer.parseInt(v)))));
					} catch (NumberFormatException fex) {
						sf.addDeclaration(new VariablesDeclaration(typeRef("double").addModifiers(ModifierType.Const), new DirectDeclarator(name, expr(Double.parseDouble(value)))));
					}
					
				} catch (Exception ex) {
					System.err.println("Parsing of enum " + name + " with value '" + value + "' failed : " + ex);
				}
			}
			
		}
	}
	private void parseConstants(Node signatures, SourceFile sf) {
		for (Node cn : XMLUtils.getChildrenByName(signatures, "constant")) {
			String name = XMLUtils.getAttribute(cn, "name");
			try {
				TypeRef tr = parseType(cn);
				if (tr == null)
					continue;
			
				sf.addDeclaration(new VariablesDeclaration(tr.addModifiers(ModifierType.Extern, ModifierType.Const), 
						new DirectDeclarator(name)));
			} catch (Exception ex) {
				System.err.println("Parsing of constant " + name + " failed : " + ex);
			}
		}
	}

	private void parseClasses(String framework, Node signatures, SourceFile sf) throws XPathExpressionException {
		for (Node classe : XMLUtils.getChildrenByName(signatures, "class")) {
			Struct cs = parseClasse(classe, Struct.Type.ObjCClass, sf);
			if (cs == null)
				continue;
			
			if (!"NSObject".equals(String.valueOf(cs.getTag().toString())))
				cs.setParents(typeRef(ident("NSObject")));
			
//			cs.accept(result);
		}
		for (Node classe : XMLUtils.getChildrenByName(signatures, "informal_protocol")) {
			Struct cs = parseClasse(classe, Struct.Type.ObjCClass, sf);
			if (cs == null)
				continue;
			
			cs.setCategoryName(cs.getTag() == null ? null : cs.getTag().toString());
			cs.setTag(ident("NSObject"));
//			cs.accept(result);
		}
	}
	private Struct parseClasse(Node classe,
			com.ochafik.lang.jnaerator.parser.Struct.Type type,
			SourceFile sf) throws XPathExpressionException {
		
		Struct cs = new Struct();
		cs.setType(type);
		String name = XMLUtils.getAttribute(classe, "name");
		if (result.config.verbose)
			System.out.println("Parsing class " + name);
		
		cs.setTag(ident(name));
		
		for (Node method : XPathUtils.findNodesIterableByXPath("method", classe)) {
//			String mname = XMLUtils.getAttribute(method, "selector");
			try {
//				if ((name.equals("NSScriptClassDescription") || name.equals("NSObject")) && mname.contains("classCode"))
//					name.toString();
				cs.addDeclaration(parseFunction(Type.ObjCMethod, method, sf));
			} catch (Exception ex) {
				ex.printStackTrace();
			}
		}
		sf.addDeclaration(new TaggedTypeRefDeclaration(cs));
		return cs;
	}

	private void parseFunctions(String framework, Node signatures, SourceFile sf) throws XPathExpressionException {
		for (Node function : XMLUtils.getChildrenByName(signatures, "function")) {
			String name = XMLUtils.getAttribute(function, "name");
			Node retval = XMLUtils.getFirstNamedNode(function, "retval");
			String already_retained = retval != null ? XMLUtils.getAttribute(retval, "already_retained") : null;
			if (already_retained != null && (already_retained = already_retained.trim()).length() > 0)
			{
				boolean alreadyRetained = "true".equals(already_retained);
				Result.getMap(result.retainedRetValFunctions, framework).put(name, alreadyRetained);
			}
			if ("true".equals(XMLUtils.getAttribute(function, "inline")))
				continue; // TODO handle inline functions : link to BridgeSupport auxiliary library
			
			try {
				Function f = parseFunction(Type.CFunction, function, sf);
				if (f == null)
					continue;
				sf.addDeclaration(f);
//				f.accept(result);
				
			} catch (Exception ex) {
				ex.printStackTrace();
			}
		}
	}
	public TypeRef parseType(Node node) throws XPathExpressionException, RecognitionException, IOException {
		if (node == null)
			return null;
		
		TypeRef declaredType = null;
		try {
			String dt = XMLUtils.getAttribute(node, "declared_type");
			if (dt != null) {
				ObjCppParser parser = new JNAeratorParser().newObjCppParser(result.typeConverter, dt, false, null);
				parser.setupScopes();
				declaredType = parser.mutableTypeRef();
			}
		} catch (Exception ex) {
			ex.printStackTrace();
		}
		
		TypeRef inferredType = null;
		if (declaredType == null || declaredType instanceof SimpleTypeRef) {
			inferredType = parseAndReconciliateType(XMLUtils.getAttribute(node, "type"), XMLUtils.getAttribute(node, "type64"));
			if (inferredType != null && declaredType instanceof SimpleTypeRef && !inferredType.toString().equals(declaredType.toString())) {
				String sn = ((SimpleTypeRef)declaredType).getName().toString();
				result.addWeakTypeDef(inferredType.clone(), sn);
			}
		}
		return declaredType != null ? declaredType : inferredType;
	}
	private Function parseFunction(Type cfunction, Node function, SourceFile sf) throws XPathExpressionException, RecognitionException, IOException {
		TypeRef tr = parseType(XMLUtils.getFirstNamedNode(function, "retval"));
		
//		if (tr == null)
//			tr = typeRef("id");
		
		String selector = XMLUtils.getAttribute(function, "selector");
		String[] splitSelector = selector == null ? null : selector.split(":");
		String name = XMLUtils.getAttribute(function, "name");
		if (name == null && splitSelector != null && splitSelector.length > 0)
			name = splitSelector[0];
		
		String type = XMLUtils.getAttribute(function, "type"), type64 = XMLUtils.getAttribute(function, "type64");
		Function methodType = null;
		if (type != null) {
			methodType = parseAndReconciliateMethods(type, type64);
			if (tr == null)
				tr = methodType.getValueType();
//			System.out.println(methodType.toString());
		}
		
		Function f = new Function(cfunction, ident(name), tr);

		String class_method = XMLUtils.getAttribute(function, "class_method");
		if ("true".equals(class_method))
			f.addModifiers(ModifierType.Static);
		
//		f.setElementFile(sf);
		int iArg = 0;
		for (Node arg : XMLUtils.getChildrenByName(function, "arg")) {//XPathUtils.findNodesIterableByXPath("arg", function)) {
			TypeRef at = parseType(arg);
			if (at == null && methodType != null) {
				if (iArg < methodType.getArgs().size())
					at = methodType.getArgs().get(iArg).getValueType();
				else
					at = null;
			}
			if (at == null)
				return null;
			Arg a = new Arg();
			a.setName(XMLUtils.getAttribute(arg, "name"));
			a.setValueType(at);
			a.setSelector(splitSelector == null || iArg >= splitSelector.length ? null : splitSelector[iArg]);
			f.addArg(a);
			iArg++;
		}
		return f;
	}

	TypeRef parseType(String mangled) throws RecognitionException, IOException {
		return newObjCDemangler(mangled, true).mangledTypeEOF();
	}
	Function parseMethod(String mangled) throws RecognitionException, IOException {
		return newObjCDemangler(mangled, true).methodType();
	}
	TypeRef parseAndReconciliateType(String mangled32, String mangled64) throws RecognitionException, IOException {
//		System.out.println("Parsing \"" + mangled32 + "\":");
		if (mangled32 == null)
			return null;
		
		TypeRef tr32 = parseType(mangled32);
		if (mangled64 != null  && mangled64.trim().length() > 0 && !mangled32.equals(mangled64)) {
//			System.out.println("Parsing \"" + mangled64 + "\":");
			TypeRef tr64 = parseType(mangled64);
		
			try {
				return (TypeRef)result.universalReconciliator.reconciliate32bitsAnd64bits(tr32, tr64);
			} catch (Exception ex) {
				ex.printStackTrace();
				return null;
			}
		} else
			return tr32;
		
	}
	Function parseAndReconciliateMethods(String mangled32, String mangled64) throws RecognitionException, IOException {
//		System.out.println("Parsing \"" + mangled32 + "\":");
		if (mangled32 == null)
			return null;
		
		Function tr32 = parseMethod(mangled32);
		if (mangled64 != null  && mangled64.trim().length() > 0 && !mangled32.equals(mangled64)) {
//			System.out.println("Parsing \"" + mangled64 + "\":");
			Function tr64 = parseMethod(mangled64);
		
			try {
				return (Function)result.universalReconciliator.reconciliate32bitsAnd64bits(tr32, tr64);
			} catch (Exception ex) {
				ex.printStackTrace();
				return null;
			}
		} else
			return tr32;
		
	}
	private void parseStructs(Node signatures, SourceFile sf) throws XPathExpressionException {
		for (Node function : XMLUtils.getChildrenByName(signatures, "struct")) {//PathUtils.findNodesIterableByXPath("signatures/struct", xml)) {
			String name = XMLUtils.getAttribute(function, "name");
//			if (name.equals("NSFastEnumerationState"))
//				name.toString();
			String type32 = XMLUtils.getAttribute(function, "type"), type64 = XMLUtils.getAttribute(function, "type64");
			try {
				try {
					TypeRef tr = parseAndReconciliateType(type32, type64);
					StoredDeclarations.TypeDef td = new StoredDeclarations.TypeDef(tr, new Declarator.DirectDeclarator(name));
					//td.addToCommentBefore("Original signature : " + type32);
					sf.addDeclaration(td);
					
//					System.out.println(td);
//					System.out.println();
					
//					if (name.equals("NSRange") || name.equals("NSSize"))
//						name.toString();
//					td.accept(result);
				} catch (Exception ex) {
					ex.printStackTrace();
					continue;
				}
				
			} catch (Exception e) {
				
				e.printStackTrace();
			}
			//Result.getMap(result.retainedRetValFunctions, framework).put(name, alreadyRetained);
			//}
		}
	}
	static ObjCDemanglingParser newObjCDemangler(String s, final boolean verbose) throws IOException {
		return new ObjCDemanglingParser(
				new CommonTokenStream(
						new ObjCDemanglingLexer(
								new ANTLRReaderStream(new StringReader(s))
						)
				)
//				, new DummyDebugEventListener()
		) {
			@Override
			public void reportError(RecognitionException arg0) {
				if (verbose) {
//					System.err.println(arg0 + " (next = '" + next() + "')");
					super.reportError(arg0);
				}
			}
		};
	}

	private void parseStringConstants(String framework, Node signatures, SourceFile sf) throws XPathExpressionException {
		for (Node string_constant : XMLUtils.getChildrenByName(signatures, "string_constant")) {
			String name = XMLUtils.getAttribute(string_constant, "name");
			String value = XMLUtils.getAttribute(string_constant, "value");
			if (value != null) {
				Result.getMap(result.stringConstants, framework).put(name, value);
			}
		}
	}
}
