/*
 * Licensed to The Apereo Foundation under one or more contributor license
 * agreements. See the NOTICE file distributed with this work for
 * additional information regarding copyright ownership.
 *
 * The Apereo Foundation licenses this file to you 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 org.unitime.commons.ant;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.util.Iterator;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.Task;
import org.dom4j.Attribute;
import org.dom4j.Comment;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.Node;
import org.dom4j.io.OutputFormat;
import org.dom4j.io.SAXReader;
import org.dom4j.io.XMLWriter;
import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

/**
 * Lower case table, column and sequence names, and foreign keys in .hbm.xml files.
 * This should negate the need to set MySQL to case insensitive mode on Linux based systems.
 * 
 * @author Tomas Muller
 *
 */
public class LowercaseTableNames extends Task {
	private SAXReader iSAXReader = null;
	private String iSource = null;
	private String iConfig = "hibernate.cfg.xml";
	
	public LowercaseTableNames() throws DocumentException, SAXException {
		iSAXReader = new SAXReader();
		iSAXReader.setEntityResolver(iEntityResolver);
		iSAXReader.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
	}
	
	private EntityResolver iEntityResolver = new EntityResolver() {
	    public InputSource resolveEntity(String publicId, String systemId) {
	        if (publicId.equals("-//Hibernate/Hibernate Mapping DTD 3.0//EN")) {
	        	return new InputSource(getClass().getClassLoader().getResourceAsStream("org/hibernate/hibernate-mapping-3.0.dtd"));
	        } else if (publicId.equals("-//Hibernate/Hibernate Configuration DTD 3.0//EN")) {
	        	return new InputSource(getClass().getClassLoader().getResourceAsStream("org/hibernate/hibernate-configuration-3.0.dtd"));
	        }
	        return null;
	    }
	};
	
	public void setSource(String source) {
		iSource = source;
	}
	
	public void setConfig(String config) {
		iConfig = config;
	}

	protected Document read(String resource) throws IOException, DocumentException {
		if (iSource == null) {
			info("  -- reading " + resource + " ...");
			return iSAXReader.read(getClass().getClassLoader().getResourceAsStream(resource));
		} else {
			info("  -- reading " + iSource + File.separator + resource + " ...");
			return iSAXReader.read(new File(iSource + File.separator + resource));
		}
	}

	protected void write(String resource, Document document) throws IOException, DocumentException {
		File file = null;
		if (iSource == null) {
			file = new File(getClass().getClassLoader().getResource(resource).getFile());
		} else {
			file = new File(iSource + File.separator + resource);
		}
		info("  -- writing " + file + " ...");
		FileOutputStream fos = new FileOutputStream(file);
		try {
			OutputFormat format = OutputFormat.createPrettyPrint();
			format.setIndentSize(4);
			format.setPadText(false);
			new MyXMLWriter(fos, format).write(document);
		} finally {
			fos.flush(); fos.close();
		}
	}
	
	protected void pretty(File f) {
	}
	
	public void execute() throws BuildException {
		try {
			convert();
		} catch (Exception e) {
			throw new BuildException(e);
		}
	}
	
	public void info(String message) {
		try {
			log(message);
		} catch (Exception e) {
			System.out.println(message);
		}
	}
	
	public void warn(String message) {
		try {
			log(message, Project.MSG_WARN);
		} catch (Exception e) {
			System.out.println(message);
		}
	}

	public void convert() throws IOException, DocumentException {
		info("Config: " + (iSource == null ? getClass().getClassLoader().getResource(iConfig) : iSource + File.separator + iConfig));
		File workDir = null;
		if (iSource == null) {
			workDir = new File(getClass().getClassLoader().getResource(iConfig).getFile());
			while (workDir.getParentFile() != null && !"WebContent".equals(workDir.getName()))
				workDir = workDir.getParentFile();
			workDir = new File(workDir.getParentFile(), "JavaSource");
			workDir.mkdirs();
		} else {
			workDir = new File(iSource);
		}
		info("Working directory: " + workDir);
		info("Reading hibernate.cfg.xml ...");
		Document document = read(iConfig);
		Element root = document.getRootElement();
		Element sessionFactoryElement = root.element("session-factory");
		for (Iterator<Element> i = sessionFactoryElement.elementIterator("mapping"); i.hasNext(); ) {
			Element m = i.next();
			String resource = m.attributeValue("resource");
			if (resource == null) continue;
			info("Processing " + resource + " ...");
			Document resDoc = read(resource);
			convert(resDoc.getRootElement());
			write(resource, resDoc);
		}
	}
	
	private String[] sAttributes = new String[] {
			"table", "sequence", "column", "foreign-key", "order-by"
	};
	
	private String[] sFormulas = new String[] {
			"formula", "where"
	};

	private String lowerFormula(String formula) {
		boolean quot = false;
		String ret = "";
		for (int i = 0; i < formula.length(); i++) {
			char ch = formula.charAt(i);
			if (ch == '\'') quot = !quot;
			ret += (quot ? ch : Character.toLowerCase(ch));
		}
		return ret.replace("%schema%", "%SCHEMA%").replace("/*+ rule */", "/*+ RULE */");
	}

	private void convert(Element element) {
		for (Iterator<Attribute> i = element.attributeIterator(); i.hasNext(); ) {
			Attribute attribute = i.next();
			for (String name: sAttributes) {
				if (name.equals(attribute.getName())) {
					if (!attribute.getValue().equals(attribute.getValue().toLowerCase())) {
						info("  -- converting " + name + " " + attribute.getValue() + " to " + attribute.getValue().toLowerCase());
					}
					attribute.setValue(attribute.getValue().toLowerCase());
				}
			}
			for (String name: sFormulas) {
				if (name.equals(attribute.getName())) {
					if (!lowerFormula(attribute.getValue()).equals(attribute.getValue())) {
						info("  -- converting "+name+": " + attribute.getValue());
						info("  -- into : " + lowerFormula(attribute.getValue()));
					}
					attribute.setValue(lowerFormula(attribute.getValue()));
				}
			}
		}
		if (element.getName().equals("param")) {
			for (String name: sAttributes) {
				if (name.equals(element.attributeValue("name", ""))) {
					if (!element.getText().equals(element.getText().toLowerCase())) {
						info("  -- converting " + name + " " + element.getText() + " to " + element.getText().toLowerCase());
					}
					element.setText(element.getText().toLowerCase());
				}
			}
		}
		for (Iterator<Element> i = element.elementIterator(); i.hasNext(); ) {
			convert(i.next());
		}
	}

	public static void main(String[] args) {
		try {
			LowercaseTableNames ltn = new LowercaseTableNames();
			ltn.setSource("/Users/muller/Sources/UniTime/JavaSource");
			ltn.convert();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
	private class MyXMLWriter extends XMLWriter {
		private OutputFormat format;
	    private int indentLevel = 0;
		
		public MyXMLWriter(OutputStream out, OutputFormat format) throws UnsupportedEncodingException {
			super(out, format);
			this.format = format;
		}
	
	    protected void indent() throws IOException {
	        String indent = format.getIndent();

	        if ((indent != null) && (indent.length() > 0)) {
	            for (int i = 0; i < indentLevel; i++) {
	                writer.write(indent);
	            }
	        }
	    }

	    protected void writeAttributes(Element element) throws IOException {
	        for (int i = 0, size = element.attributeCount(); i < size; i++) {
	            Attribute attribute = element.attribute(i);
                char quote = format.getAttributeQuoteCharacter();
                if (element.attributeCount() > 2) {
                    writePrintln();
                    indent();
                    writer.write(format.getIndent());
                } else {
                	writer.write(" ");
                }
                writer.write(attribute.getQualifiedName());
                writer.write("=");
                writer.write(quote);
                writeEscapeAttributeEntities(attribute.getValue());
                writer.write(quote);
	        }
	    }
	    
	    protected void writeElement(Element element) throws IOException {
	        int size = element.nodeCount();
	        String qualifiedName = element.getQualifiedName();

	        writePrintln();
	        indent();

	        writer.write("<");
	        writer.write(qualifiedName);

	        boolean textOnly = true;

	        for (int i = 0; i < size; i++) {
	            Node node = element.node(i);
	            if (node instanceof Element) {
	                textOnly = false;
	            } else if (node instanceof Comment) {
	                textOnly = false;
	            }
	        }

	        writeAttributes(element);

	        lastOutputNodeType = Node.ELEMENT_NODE;

	        if (size <= 0) {
	            writeEmptyElementClose(qualifiedName);
	        } else {
	            writer.write(">");

	            if (textOnly) {
	                // we have at least one text node so lets assume
	                // that its non-empty
	                writeElementContent(element);
	            } else {
	            	if (element.attributeCount() > 3)
	            		writePrintln();
	                // we know it's not null or empty from above
	                ++indentLevel;

	                writeElementContent(element);

	                --indentLevel;

	                writePrintln();
	                indent();
	            }

	            writer.write("</");
	            writer.write(qualifiedName);
	            writer.write(">");
	        }
        	if (element.attributeCount() > 2 && indentLevel > 0)
        		writePrintln();

	        lastOutputNodeType = Node.ELEMENT_NODE;
	    }

	}
}