/**
 * License Agreement.
 *
 * Rich Faces - Natural Ajax for Java Server Faces (JSF)
 *
 * Copyright (C) 2007 Exadel, Inc.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License version 2.1 as published by the Free Software Foundation.
 *
 * This library 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 this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301  USA
 */

package org.ajax4jsf.builder.generator;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.io.Writer;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import org.ajax4jsf.builder.config.BuilderConfig;
import org.ajax4jsf.builder.config.ComponentBean;
import org.ajax4jsf.builder.config.RendererBean;
import org.ajax4jsf.templatecompiler.builder.CompilationContext;
import org.ajax4jsf.templatecompiler.builder.CompilationException;
import org.ajax4jsf.templatecompiler.builder.TemplateCompiler;
import org.ajax4jsf.templatecompiler.elements.RootElement;
import org.ajax4jsf.templatecompiler.elements.TemplateElement;
import org.ajax4jsf.templatecompiler.elements.vcp.FResourceTemplateElement;
import org.ajax4jsf.templatecompiler.elements.vcp.HeaderResourceElement;
import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

/**
 * @author Nick - mailto:nbelaevski@exadel.com
 * created 17.04.2007
 * 
 */
public class ResourcesConfigGenerator extends FacesConfigGenerator {

	private File resourcesConfig;
	private File templatesDirectory;

	private static final SAXParserFactory parserFactory = SAXParserFactory.newInstance();
	
	public ResourcesConfigGenerator(JSFGeneratorConfiguration task, Logger log) {
		super(task, log);
	}

	private void addResources(ResourcesConfigGeneratorBean configBean, TemplateElement templateElement,
			String packageName, RendererBean renderer, BuilderConfig builderConfig) throws ClassNotFoundException {
		if (templateElement instanceof FResourceTemplateElement) {
			FResourceTemplateElement resourceTemplateElement = (FResourceTemplateElement) templateElement;
			String name = resourceTemplateElement.getName();
			
			addResource(name, packageName, configBean);
		} else if (templateElement instanceof RootElement) {
			RootElement rootElement = (RootElement) templateElement;
			
			CompilationContext compilationContext = rootElement.getComponentBean();
			
			String classname = renderer.getClassname();
			int idx = classname.lastIndexOf('.');
			if (idx != -1) {
				packageName = classname.substring(0, idx);
			} else {
				packageName = "";
			}
			
			ClassLoader loader = builderConfig.getLoader();
			Class cl = null;
			
			try {
				cl = loader.loadClass(classname);
			} catch (ClassNotFoundException e) {
				String superclass = compilationContext.getBaseclassPackageName() + "." + compilationContext.getBaseclassName();
				if (superclass != null) {
					cl = loader.loadClass(superclass);
				}
			}

			if (cl != null) {
				GetResourceInterceptor interceptor = new GetResourceInterceptor();
				Object instance = AbstractClassStubBuilder.buildStub(cl, interceptor,
						builderConfig.getLoader());
				
				try {
					Method method = null;
					Object object = null;
					
					Class<?> cl1 = instance.getClass();
					while (cl1 != null && method == null) {
						try {
							method = cl1.getDeclaredMethod("getStyles", null);
						} catch (NoSuchMethodException e) {
							cl1 = cl1.getSuperclass();
						}
					}
					
					if (method != null) {
						method.setAccessible(true);
						object = method.invoke(instance, null);
						List<String> list = interceptor.getList();
						if (list != null) {
							for (String name : list) {
								addResource(name, packageName, configBean);
							}
						}
					}
					
					interceptor.clearList();

					method = null;
					cl1 = instance.getClass();
					while (cl1 != null && method == null) {
						try {
							method = cl1.getDeclaredMethod("getScripts", null);
						} catch (NoSuchMethodException e) {
							cl1 = cl1.getSuperclass();
						}
					}
					
					if (method != null) {
						method.setAccessible(true);
						object = method.invoke(instance, null);
						List<String> list = interceptor.getList();
						if (list != null) {
							for (String name : list) {
								addResource(name, packageName, configBean);
							}
						}
					}
				} catch (SecurityException e) {
					builderConfig.getLog().error(e.getMessage(), e);
				} catch (IllegalArgumentException e) {
					builderConfig.getLog().error(e.getMessage(), e);
				} catch (IllegalAccessException e) {
					builderConfig.getLog().error(e.getMessage(), e);
				} catch (InvocationTargetException e) {
					builderConfig.getLog().error(e.getMessage(), e);
				}
			}
		} else if (templateElement instanceof HeaderResourceElement) {
			HeaderResourceElement resourceElement = (HeaderResourceElement) templateElement;
			String[] paths = resourceElement.getResourcePaths();
			if (paths != null) {
				for (int i = 0; i < paths.length; i++) {
					String string = paths[i];
					
					addResource(string, packageName, configBean);
				}
			}
		}
		
		ArrayList<TemplateElement> subElements = templateElement.getSubElements();
		for (TemplateElement element : subElements) {
			addResources(configBean, element, packageName, renderer, builderConfig);
		}
	}

	private void addResource(String name, String packageName,
			ResourcesConfigGeneratorBean configBean) {

		String resolvedPath = resolveResourcePath(name, packageName);
		// remove leading / from resource name.
	        if(name.startsWith("/")){
	            name = name.substring(1);
	        }
		if (resolvedPath != null) {
			configBean.addPathResource(name, resolvedPath);
		} else {
			//couldn't resolve, treat as class name
			configBean.addClassResource(name, name);
		}
	}
	
	private String resolveResourcePath(String name, String packageName) {
		if (name.contains("/")) {
			String resolvedName;

			if (!name.startsWith("/")) {
				//need to resolve
				StringBuffer normalizedName = new StringBuffer();
				normalizedName.append(packageName.replace('.', '/'));
				
				if (!packageName.endsWith("/")) {
					normalizedName.append('/');
				}

				normalizedName.append(name);
				
				resolvedName = normalizedName.toString();
			} else {
				if (name.length() > 0) {
					resolvedName = name.substring(1);
				} else {
					resolvedName = null;
				}
			}

			return resolvedName;
		} 
		
		return null;
	}

	private void addResources(ResourcesConfigGeneratorBean configBean, RendererBean renderer, BuilderConfig builderConfig) throws CompilationException, IOException, ClassNotFoundException {
		if (null != renderer && renderer.isGenerate()
				&& null != renderer.getTemplate()) {

			File template;
			if (null != getTemplates()) {
				template = new File(getTemplates(), renderer.getTemplate());
			} else {
				template = new File(renderer.getTemplate());
			}
			CompilationContext rendererBean = new RendererCompilationContext(
					getLog(), getClassLoader(),getConfig());

			TemplateCompiler templateCompiler = new TemplateCompiler();
			InputStream templateStream = new FileInputStream(template);
			templateCompiler.processing(templateStream, rendererBean);
		
			TemplateElement root = rendererBean.getTree();
			
			String classname = renderer.getClassname();
			String packageName;
			int idx = classname.lastIndexOf('.');
			if (idx != -1) {
				packageName = classname.substring(0, idx);
			} else {
				packageName = "";
			}
			
			addResources(configBean, root, packageName, renderer, builderConfig);
		}
	}

	private void parseXCSSResource(String resourcePath, final ResourcesConfigGeneratorBean bean) throws SAXException, IOException, ParserConfigurationException {
	    InputStream resourceStream = getClassLoader().getResourceAsStream(resourcePath);
	    if (resourceStream != null) {
		debug("XCSS file exists in classpath");

		try {
		    SAXParser parser = parserFactory.newSAXParser();
		    parser.parse(resourceStream, new DefaultHandler() {
			@Override
			public void startElement(String uri,
				String localName, String name,
				Attributes attributes)
			throws SAXException {

			    super.startElement(uri, localName, name, attributes);

			    if ("f:resource".equals(name)) {
				String value = attributes.getValue("f:key");

				if (value != null) {
				    debug("Adding resource: " + value);

				    addResource(value, "", bean);
				}
			    }
			}
		    });

		} finally {
		    try {
			resourceStream.close();
		    } catch (IOException e) {
			getLog().error(e.getLocalizedMessage(), e);
		    }
		}
	    } else {
		getLog().error("Resource " + resourcePath + " hasn't been found!");
	    }
	}
	
	private boolean isXCSSPath(String resourcePath) {
	    return resourcePath != null && resourcePath.endsWith(".xcss");	
	}
	
	public void createFiles(BuilderConfig config) throws GeneratorException {
		VelocityContext context = new VelocityContext();
		Template template = getTemplate();
		try {
			// Put common properties

			final ResourcesConfigGeneratorBean bean = new ResourcesConfigGeneratorBean();
			
			List<ComponentBean> components = config.getComponents();
			for (ComponentBean componentBean : components) {
				RendererBean rendererBean = componentBean.getRenderer();

				addResources(bean, rendererBean, config);
			}

			List<RendererBean> renderers = config.getRenderers();
			for (RendererBean rendererBean : renderers) {
				addResources(bean, rendererBean, config);
			}

			String includedContent = getIncludeContent();
			if (includedContent != null && includedContent.length() != 0) {
			    String parseableContent = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><resource-config>" + includedContent + 
			    	"</resource-config>";

				SAXParser parser = parserFactory.newSAXParser();
				parser.parse(new InputSource(new StringReader(parseableContent)), new DefaultHandler() {
					private StringBuilder path;
					
				    	@Override
					public void startElement(String uri,
							String localName, String name,
							Attributes attributes)
							throws SAXException {

						super.startElement(uri, localName, name, attributes);
					
						if ("path".equals(name)) {
						    this.path = new StringBuilder();
						}
					}
					
				    	public void characters(char[] ch, int start, int length) throws SAXException {
				    	    if (this.path != null) {
				    		this.path.append(ch, start, length);
				    	    }
				    	};
				    	
					@Override
					public void endElement(String uri, String localName, String name) throws SAXException {
					    super.endElement(uri, localName, name);

					    if ("resource".equals(name)) {
						if (this.path != null && this.path.length() != 0) {
						    String resourcePath = this.path.toString().trim();
						    if (isXCSSPath(resourcePath)) {
							debug("XCSS file detected: " + resourcePath);
							try {
							    parseXCSSResource(resourcePath, bean);
							} catch (IOException e) {
							    throw new SAXException(e.getLocalizedMessage(), e);
							} catch (ParserConfigurationException e) {
							    throw new SAXException(e.getLocalizedMessage(), e);
							}
						    }
						}
						this.path = null;
					    }
					}
				});
			}
			
			Set<String> pathResourcesSet = new LinkedHashSet<String>(bean.getPathResources().values());
			for (Iterator<String> iterator = pathResourcesSet.iterator(); iterator
					.hasNext();) {
				String resourcePath = iterator.next();
				
				if (isXCSSPath(resourcePath)) {
				    debug("XCSS file detected: " + resourcePath);
				    parseXCSSResource(resourcePath, bean);
				}
			}
			
			context.put("classResources", bean.getClassResources());
			context.put("pathResources", bean.getPathResources());
			context.put("resourcesConfig", this);
			
			File configFile = getResourcesConfig();
			File javaDir = configFile.getParentFile();
			if (!javaDir.exists()) {
				javaDir.mkdirs();
			}
			if (configFile.exists()) {
				configFile.delete();
			}
			Writer out = new BufferedWriter(new FileWriter(configFile));
			template.merge(context, out);
			out.flush();
			out.close();
		} catch (Exception e) {
			throw new GeneratorException("Error create new resources-config.xml ",
					e);
		}
	}

	protected String getRootTag() {
		return "resource-config";
	}

	public File getResourcesConfig() {
		return resourcesConfig;
	}

	public void setResourcesConfig(File resourcesConfig) {
		this.resourcesConfig = resourcesConfig;
	}

	protected String getDefaultTemplateName() {
		return "resources-config.vm";
	}

	public void setTemplates(File templatesDirectory) {
		this.templatesDirectory = templatesDirectory;
	}

	public File getTemplates() {
		return this.templatesDirectory;
	}
}

class GetResourceInterceptor implements MethodInterceptor {

	private static final Class<?>[] SIGNATURE = new Class<?>[] {
		String.class
	};
	
	private List<String> list = new ArrayList<String>();
	
	public List<String> getList() {
		return list;
	}
	
	public void clearList() {
		list.clear();
	}
	
	public Object intercept(Object instance, Method method, Object[] args,
			MethodProxy methodProxy) throws Throwable {

		if ("getResource".equals(method.getName()) && 
				Arrays.equals(SIGNATURE, method.getParameterTypes())) {

			list.add((String) args[0]);
			
			return null;
		} else {
			return methodProxy.invokeSuper(instance, args);
		}
	}
	
}