package de.spieleck.config;

import de.spieleck.net.URLTools;

import java.io.*;

import java.net.*;

import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.StringTokenizer;

import javax.xml.parsers.*;

import org.xml.sax.*;

/**
 * Parser and builder for the "easy" XML based configuration classes.
 */
public class Config
{
    public final static String INCLUDEELEM    = "se:include";
    public final static String PARAMELEM      = "se:param";
    public final static String THISATTR       = "se:this";
    public final static String NAME_ATTR      = "se:name";
    public final static String VALUE_ATTR     = "se:value";
    public final static char TEXTSEPARATOR  = ' ';
    public final static String SETUPEXTENSION = ".conf";
    public final static String INC_HREF       = "href";
    public final static String INC_PATH       = "path";
    public final static String INC_LIST       = "list";
    public final static String INC_DIR        = "dir";
    public final static String INC_EXCL       = "exclude";
    public final static String INC_SEP        = "@";
    protected static List listeners;

    // Semi-Singleton: The default setup is an singleton while still
    // many setup objects may coexist.
    protected static ConfigFileNode defaultConfig = null;

    private Config()
    {
    }

    /**
     * Parse setup tree from an InputSource
     */
    public static ConfigFileNode parse(InputSource is)
     throws IOException, SAXException
    {
        return parse(is, NullParamMap.getInstance());
    }

    public static ConfigFileNode parse(InputSource is, ConfigParamMap pm)
     throws IOException, SAXException
    {
        ConfigFileNode top       = new ConfigFileNode(null, null, 
                                                      is.getSystemId(), pm);
        ConfigSaxHandler handler = new ConfigSaxHandler(top, pm);
        try
        {
            SAXParser parser = newSAXParser();
            parser.parse(is, handler);
        }
        catch (Exception e)
        {
            e.printStackTrace();
            throw new SAXException(e);
        }

        return top;
    }

    /** Hold a SaxParserFactory to instantiate parsers only */
    protected static SAXParserFactory spf = null;

    /** obtain a new parser */
    protected static synchronized SAXParser newSAXParser()
     throws SAXException, ParserConfigurationException
    {
        if (spf == null)
        {
            spf = SAXParserFactory.newInstance();
            spf.setNamespaceAware(false);
            spf.setValidating(false);
        }

        return spf.newSAXParser();
    }

    public static synchronized ConfigNode setConfig(ConfigFileNode setup)
    {
        ConfigFileNode old = defaultConfig;
        defaultConfig = setup;
        handleChange(setup);
        return old;
    }

    public static synchronized ConfigNode getConfig()
    {
        return defaultConfig;
    }

    /**
   * We supply a preliminary api to inform listeners about a
   * change in configuration.
   */
    public synchronized static void addListener(ConfigListener listener)
    {
        if (listeners == null)
            listeners = new LinkedList();
        listeners.add(listener);
    }

    public synchronized static void removeListener(ConfigListener listener)
    {
        if (listeners != null)
            listeners.remove(listener);
    }

    protected static void handleChange(ConfigNode s)
    {
        if (listeners == null)
            return;
        Iterator e = listeners.iterator();
        while (e.hasNext())
        {
            ConfigListener listener = (ConfigListener)e.next();
            listener.handleConfigChange(s);
        }
    }

    /**
   * Subclass to handle parser events
   */
    protected final static class ConfigSaxHandler
     extends HandlerBase
    {
        private ConfigParamMap pm;
        private Locator locator;
        private ConfigNodeImpl node;
        private StringBuffer text = new StringBuffer(200);
        private boolean hasThis;

        ConfigSaxHandler(ConfigNodeImpl top, ConfigParamMap pm)
        {
            this.node = top;
            this.pm = pm;
        }

        public void setDocumentLocator(Locator locator)
        {
            this.locator = locator;
        }

        public void startElement(String name, AttributeList attrs)
         throws SAXException
        {
            if (text.length() != 0)
                configError("subnodes must preceede text '"+name+"'", null);
            hasThis = false;
            if (name.equals(PARAMELEM))
                doParam(attrs);
            else if (name.equals(INCLUDEELEM))
                doInclude(attrs);
            else
            {
                node = node.addChild(name, null);
                int length = attrs.getLength();
                for (int i = 0; i < length; i++)
                {
                    String key   = attrs.getName(i);
                    String value = attrs.getValue(i);
                    if (key.equals(THISATTR))
                    {
                        hasThis = true;
                        node.setValue(value);
                    }
                    else if ( !key.startsWith("xmlns:") )
                        node.addChild(key, value);
                }
            }
        }

        protected void doParam(AttributeList atts)
            throws SAXException
        {
            String name = atts.getValue(NAME_ATTR);
            String value = atts.getValue(VALUE_ATTR);
            if ( name == null || value == null )
             configError(PARAMELEM+" needs "+NAME_ATTR+" and "+VALUE_ATTR,null);
            pm.set(name, value);
        }

        /**
         * obtainExtraConfig
         *
         * obtains the first existing file out of a list.
         * the syntax for this list is:
         *
         * you can mix relative with absolute paths. separate them with a comma
         * mark a relative path with 'href' and an absolute one with 'path' like this
         * href@relativepath or path@absolutepath
         *
         * @param   String  comma-separated list with relative or absolute pathinformation [href/path@relative/absolute-path]
         * @return  URL     tested URL with the first existing configuration file
         */
        private URL obtainExtraConfig(String pathlist)
         throws SAXException, IOException
        {
            StringTokenizer st = new StringTokenizer(pathlist, ",");
            while (st.hasMoreTokens())
            {
                String path         = st.nextToken().trim();
                StringTokenizer str = new StringTokenizer(path, INC_SEP);
                if (str.countTokens() != 2)
                    configError(INCLUDEELEM
                                     + " with list needs the syntax "
                                     + INC_HREF + INC_SEP
                                     + "relativepath or " + INC_PATH
                                     + INC_SEP + "absolutepath", null);
                String type = str.nextToken();
                path = str.nextToken();
                URL url = null;
                if (INC_HREF.equals(type))
                {
                    url = getHrefURL(path);
                    try
                    {
                        url.openStream();

                        return url;
                    }
                    catch (IOException iox)
                    {
                        configError("path error: ", iox);
                    }
                }
                else if (INC_PATH.equals(type))
                {
                    if (new File(path).exists())
                    {
                        url = getPathURL(path);

                        return url;
                    }
                    else
                        configError(INCLUDEELEM + ": " + path
                                         + " not found", null);
                }
            }

            return null;
        }

        private URL getHrefURL(String href)
         throws SAXException, IOException
        {
            String sid = locator.getSystemId();
            if (sid == null)
                configError(INCLUDEELEM
                             + " with href needs a SystemId with inputSource", 
                             null);

            // Construct a valid relative URL!
            return new URL(new URL(sid), href);
        }

        private URL getPathURL(String path)
         throws IOException
        {
            return URLTools.toURL(path);
        }

        private URL[] getDirURL(String type, String dir, String exclude)
         throws SAXException, IOException
        {
            if (INC_HREF.equals(type))
            {
                dir = getHrefURL(dir).getPath();
            }
            final List fv = new LinkedList();
            if (exclude != null)
            {
                StringTokenizer str = new StringTokenizer(exclude, ",");
                while (str.hasMoreTokens())
                    fv.add(str.nextToken());
            }
            File file = new File(dir);
            if (!file.exists())
                configError(INCLUDEELEM+" with dir: "+dir+" not found",null);
            File[] files = file.listFiles(new FileFilter() {
                public boolean accept(File name)
                {
                    String fname = name.toString();
                    if (fv.contains(name.getName()))
                        return false;
                    return fname.endsWith(SETUPEXTENSION);
                }
            });
            URL[] url = new URL[files.length];
            for (int i = 0; i < url.length; i++)
                url[i] = getPathURL(files[i].toString());

            return url;
        }

        private void doInclude(AttributeList args)
         throws SAXException
        {
            try
            {
                int count   = 0;
                String href = args.getValue(INC_HREF);
                if (href != null)
                    count++;
                String path = args.getValue(INC_PATH);
                if (path != null)
                    count++;
                String list = args.getValue(INC_LIST);
                if (list != null)
                    count++;
                String dir = args.getValue(INC_DIR);
                if (dir != null)
                    count++;
                if (count != 1)
                    configError(INCLUDEELEM + " needs one of '"
                                     + INC_PATH + "', '" + INC_HREF
                                     + "', '" + INC_LIST + "' or '"
                                     + INC_DIR + "' attribute", null);
                String exclude = args.getValue(INC_EXCL);
                if (exclude != null && dir == null)
                    configError(INCLUDEELEM + " " + INC_EXCL
                                     + " only valid with " + INC_DIR
                                     + ".", null);
                //
                URL[] url = null;
                if (href != null)
                {
                    url = new URL[] { getHrefURL(href) };
                }
                else if (path != null)
                {
                    url = new URL[] { getPathURL(path) };
                }
                else if (list != null)
                {
                    url = new URL[] { obtainExtraConfig(list) };
                }
                else if (dir != null)
                {
                    StringTokenizer str = new StringTokenizer(dir, 
                                                              INC_SEP);
                    if (str.countTokens() == 2)
                        url = getDirURL(str.nextToken(), 
                                        str.nextToken(), exclude);
                    else
                        configError(INCLUDEELEM + " with attribute '"
                                         + INC_DIR + "': " + INC_HREF
                                         + "/" + INC_PATH + INC_SEP
                                         + "directory e.g. " + INC_HREF
                                         + INC_SEP + "d:\tmp", null);
                }
                parseIncludes(url);
            }
            catch (IOException e)
            {
                configError("include has IO problem :", e);
            }
        }

        private void parseIncludes(URL[] url)
         throws SAXException, IOException
        {
            for (int i = 0; i < url.length; i++)
            {
                InputSource is = new InputSource(url[i].toExternalForm());
                ConfigNodeImpl incNode = null;
                try
                {
                    incNode = Config.parse(is);
                }
                catch ( SAXException e )
                {
                    // We try to proceed after unparseable includes!
                    e.printStackTrace();
                }

                // Though the incNode is spurious from the logical structure
                // it is registered here to keep the file system layout!!!
                // Note therefore we can even monitor changes in an included
                // file which does not generate any nodes!
                if ( incNode != null )
                {
                    ConfigFileNode branch = node.getBranchNode();
                    if (branch != null)
                        branch.addSubReader(incNode);
                    if (incNode != null )
                        node.copyChildren(incNode);
                }
            }
        }

        //*******************************************************************
        // The remaining DocumentHandler Implementation
        //
        public void endElement(String name)
         throws SAXException
        {
            if (!name.equals(INCLUDEELEM) && !name.equals(PARAMELEM) )
            {
                if (text.length() != 0)
                {
                    node.setValue(text.toString());
                    text.setLength(0);
                }

                // XXX another semigood cast!!
                node = (ConfigNodeImpl)node.getParent();
            }
            else if (text.length() != 0 )
                configError(name+" nodes must not have text.", null);
        }

        public void characters(char[] chars, int offset, int length)
         throws SAXException
        {
            // trim leading and trailing whitespace
            for (; length > 0; length--)
            {
                if (!Character.isWhitespace(chars[offset]))
                    break;
                offset++;
            }
            for (; length > 0; length--)
                if (!Character.isWhitespace(chars[offset + length - 1]))
                    break;
            if (length <= 0)
                return;
            if (hasThis)
                configError("Mixes use of this and text at '"
                                 + new String(chars, offset, length)
                                 + "'", null);
            if (text.length() != 0)
                text.append(TEXTSEPARATOR);
            text.append(chars, offset, length);
        }

        //*******************************************************************
        // The ErrorHandler Implementation
        //
        public void warning(SAXParseException e)
        {
            showError("warning", e);
        }

        public void error(SAXParseException e)
        {
            showError("error", e);
        }

        public void fatalError(SAXParseException e)
        {
            showError("fatal", e);
        }

        private void showError(String type, SAXParseException e)
        {
            String inputFile = e.getSystemId();
            if (inputFile == null)
                inputFile = "input file";
            System.err.println(
                    "Parser " + type + " @" + inputFile + ",line "
                    + e.getLineNumber() + ",column "
                    + e.getColumnNumber());
            System.err.println("   " + e.getMessage());
        }

        private void configError(String text, Exception e)
          throws SAXParseException
        {
            SAXParseException spe;
            if (e == null)
                spe = new SAXParseException(text, locator);
            else
                spe = new SAXParseException(text, locator, e);
            showError("configError", spe);
            throw spe;
        }
    }

}

//
//    Jacson - Text Filtering with Java.
//    Copyright (C) 2002 Frank S. Nestel (nestefan -at- users.sourceforge.net)
//
//    This library 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 2.1 of the License, or (at your option) any later version.
//
//    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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
//