// License: GPL. Copyright 2007 by Immanuel Scholz and others
package org.openstreetmap.josm.tools;

import java.io.Reader;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Stack;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

import javax.xml.parsers.SAXParserFactory;

import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

/**
 * An helper class that reads from a XML stream into specific objects.
 *
 * @author Imi
 */
public class XmlObjectParser implements Iterable<Object> {

    public static final String lang = LanguageInfo.getLanguageCodeXML();
    public static class Uniform<T> implements Iterable<T>{
        private Iterator<Object> iterator;
        /**
         * @param klass This has to be specified since generics are ereased from
         * class files so the JVM cannot deduce T itself.
         */
        public Uniform(Reader input, String tagname, Class<T> klass) {
            XmlObjectParser parser = new XmlObjectParser();
            parser.map(tagname, klass);
            parser.start(input);
            iterator = parser.iterator();
        }
        public Iterator<T> iterator() {
            return new Iterator<T>(){
                public boolean hasNext() {return iterator.hasNext();}
                @SuppressWarnings("unchecked") public T next() {return (T)iterator.next();}
                public void remove() {iterator.remove();}
            };
        }
    }

    private class Parser extends DefaultHandler {
        Stack<Object> current = new Stack<Object>();
        String characters = "";
        @Override public void startElement(String ns, String lname, String qname, Attributes a) throws SAXException {
            if (mapping.containsKey(qname)) {
                Class<?> klass = mapping.get(qname).klass;
                try {
                    current.push(klass.newInstance());
                } catch (Exception e) {
                    throw new SAXException(e);
                }
                for (int i = 0; i < a.getLength(); ++i)
                    setValue(a.getQName(i), a.getValue(i));
                if (mapping.get(qname).onStart)
                    report();
                if (mapping.get(qname).both)
                {
                    try {
                        queue.put(current.peek());
                    } catch (InterruptedException e) {
                    }
                }
            }
        }
        @Override public void endElement(String ns, String lname, String qname) throws SAXException {
            if (mapping.containsKey(qname) && !mapping.get(qname).onStart)
                report();
            else if (characters != null && !current.isEmpty()) {
                setValue(qname, characters.trim());
                characters = "";
            }
        }
        @Override public void characters(char[] ch, int start, int length) {
            String s = new String(ch, start, length);
            characters += s;
        }

        private void report() {
            try {
                queue.put(current.pop());
            } catch (InterruptedException e) {
            }
            characters = "";
        }

        private Object getValueForClass(Class<?> klass, String value) {
            if (klass == Boolean.TYPE)
                return parseBoolean(value);
            else if (klass == Integer.TYPE || klass == Long.TYPE)
                return Long.parseLong(value);
            else if (klass == Float.TYPE || klass == Double.TYPE)
                return Double.parseDouble(value);
            return value;
        }

        private void setValue(String fieldName, String value) throws SAXException {
            if (fieldName.equals("class") || fieldName.equals("default") || fieldName.equals("throw") || fieldName.equals("new") || fieldName.equals("null"))
                fieldName += "_";
            try {
                Object c = current.peek();
                Field f = null;
                try {
                    f = c.getClass().getField(fieldName);
                } catch (NoSuchFieldException e) {
                    if(fieldName.startsWith(lang))
                    {
                        String locfieldName = "locale_" +
                        fieldName.substring(lang.length());
                        try {
                            f = c.getClass().getField(locfieldName);
                        } catch (NoSuchFieldException ex) {
                        }
                    }
                }
                if (f != null && Modifier.isPublic(f.getModifiers()))
                    f.set(c, getValueForClass(f.getType(), value));
                else {
                    if(fieldName.startsWith(lang))
                    {
                        int l = lang.length();
                        fieldName = "set" + fieldName.substring(l,l+1).toUpperCase() + fieldName.substring(l+1);
                    }
                    else
                    {
                        fieldName = "set" + fieldName.substring(0,1).toUpperCase() + fieldName.substring(1);
                    }
                    Method[] methods = c.getClass().getDeclaredMethods();
                    for (Method m : methods) {
                        if (m.getName().equals(fieldName) && m.getParameterTypes().length == 1) {
                            m.invoke(c, new Object[]{getValueForClass(m.getParameterTypes()[0], value)});
                            return;
                        }
                    }
                }
            } catch (Exception e) {
                e.printStackTrace(); // SAXException does not dump inner exceptions.
                throw new SAXException(e);
            }
        }
        private boolean parseBoolean(String s) {
            return s != null &&
                !s.equals("0") &&
                !s.startsWith("off") &&
                !s.startsWith("false") &&
                !s.startsWith("no");
        }
    }

    private static class Entry {
        Class<?> klass;
        boolean onStart;
        boolean both;
        public Entry(Class<?> klass, boolean onStart, boolean both) {
            super();
            this.klass = klass;
            this.onStart = onStart;
            this.both = both;
        }
    }

    private Map<String, Entry> mapping = new HashMap<String, Entry>();
    private Parser parser;

    /**
     * The queue of already parsed items from the parsing thread.
     */
    private BlockingQueue<Object> queue = new ArrayBlockingQueue<Object>(10);

    /**
     * This stores one item retrieved from the queue to give hasNext a chance.
     * So this is also the object that will be returned on the next call to next().
     */
    private Object lookAhead = null;

    /**
     * This object represent the end of the stream (null is not allowed as
     * member in class Queue).
     */
    private Object EOS = new Object();

    public XmlObjectParser() {
        parser = new Parser();
    }

    public Iterable<Object> start(final Reader in) {
        new Thread("XML Reader"){
            @Override public void run() {
                try {
                    SAXParserFactory.newInstance().newSAXParser().parse(new InputSource(in), parser);
                } catch (Exception e) {
                    try {
                        queue.put(e);
                    } catch (InterruptedException e1) {
                    }
                }
                parser = null;
                try {
                    queue.put(EOS);
                } catch (InterruptedException e) {
                }
            }
        }.start();
        return this;
    }

    public void map(String tagName, Class<?> klass) {
        mapping.put(tagName, new Entry(klass,false,false));
    }

    public void mapOnStart(String tagName, Class<?> klass) {
        mapping.put(tagName, new Entry(klass,true,false));
    }

    public void mapBoth(String tagName, Class<?> klass) {
        mapping.put(tagName, new Entry(klass,false,true));
    }

    /**
     * @return The next object from the xml stream or <code>null</code>,
     * if no more objects.
     */
    public Object next() throws SAXException {
        fillLookAhead();
        if (lookAhead == EOS)
            throw new NoSuchElementException();
        Object o = lookAhead;
        lookAhead = null;
        return o;
    }

    private void fillLookAhead() throws SAXException {
        if (lookAhead != null)
            return;
        try {
            lookAhead = queue.take();
            if (lookAhead instanceof SAXException)
                throw (SAXException)lookAhead;
            else if (lookAhead instanceof RuntimeException)
                throw (RuntimeException)lookAhead;
            else if (lookAhead instanceof Exception)
                throw new SAXException((Exception)lookAhead);
        } catch (InterruptedException e) {
            throw new RuntimeException("XmlObjectParser must not be interrupted.", e);
        }
    }

    public boolean hasNext() throws SAXException {
        fillLookAhead();
        return lookAhead != EOS;
    }

    public Iterator<Object> iterator() {
        return new Iterator<Object>(){
            public boolean hasNext() {
                try {
                    return XmlObjectParser.this.hasNext();
                } catch (SAXException e) {
                    e.printStackTrace();
                    throw new RuntimeException(e);
                }
            }
            public Object next() {
                try {
                    return XmlObjectParser.this.next();
                } catch (SAXException e) {
                    e.printStackTrace();
                    throw new RuntimeException(e);
                }
            }
            public void remove() {
                throw new UnsupportedOperationException();
            }
        };
    }
}
