/*
 * Licensed to the Ant-Contrib Project under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The Ant-Contrib 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 net.sf.antcontrib.cpptasks.apple;

import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.AttributesImpl;

import javax.xml.transform.Result;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.sax.SAXTransformerFactory;
import javax.xml.transform.sax.TransformerHandler;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.io.File;
import java.io.IOException;
import java.io.FileOutputStream;

/**
 * Static class that provides methods to serialize
 * a Map to a Cocoa XML Property List.  Does not currently support
 * date or data elements.
 */
public final class PropertyListSerialization {
    /**
     * Private constructor.
     */
    private PropertyListSerialization() {

    }

    /**
     * Serializes a property list into a Cocoa XML Property List document.
     * @param propertyList property list.
     * @param file destination.
     * @param comments comments to insert into document.
     * @throws SAXException if exception during serialization.
     * @throws TransformerConfigurationException if exception creating serializer.
     */
    public static void serialize(final Map propertyList,
                                 final List comments,
                                 final File file)
           throws IOException, SAXException,
               TransformerConfigurationException {
        SAXTransformerFactory sf = (SAXTransformerFactory)
                SAXTransformerFactory.newInstance();
        TransformerHandler handler = sf.newTransformerHandler();

        FileOutputStream os = new FileOutputStream(file);
        StreamResult result = new StreamResult(os);
        handler.setResult(result);

        handler.startDocument();
        for(Iterator iter = comments.iterator(); iter.hasNext();) {
            char[] comment = String.valueOf(iter.next()).toCharArray();
            handler.comment(comment, 0, comment.length);
        }
        AttributesImpl attributes = new AttributesImpl();
        handler.startElement(null, "plist", "plist", attributes);
        serializeMap(propertyList, handler);
        handler.endElement(null, "plist", "plist");

        handler.endDocument();
    }

    /**
     * Serialize a map as a dict element.
     * @param map map to serialize.
     * @param handler destination of serialization events.
     * @throws SAXException if exception during serialization.
     */
    private static void serializeMap(final Map map,
                                     final ContentHandler handler)
         throws SAXException {
        AttributesImpl attributes = new AttributesImpl();
        handler.startElement(null, "dict", "dict", attributes);

        if (map.size() > 0) {
            //
            //   need to output with sorted keys to maintain
            //     reproducability
            //
            Object[] keys = map.keySet().toArray();
            Arrays.sort(keys);
            for(int i = 0; i < keys.length; i++) {
                String key = String.valueOf(keys[i]);
                handler.startElement(null, "key", "key", attributes);
                handler.characters(key.toCharArray(), 0, key.length());
                handler.endElement(null, "key", "key");
                serializeObject(map.get(keys[i]), handler);
            }
        }
        handler.endElement(null, "dict", "dict");
    }

    /**
     * Serialize a list as an array element.
     * @param list list to serialize.
     * @param handler destination of serialization events.
     * @throws SAXException if exception during serialization.
     */
    private static void serializeList(final List list,
                                      final ContentHandler handler)
         throws SAXException {
        AttributesImpl attributes = new AttributesImpl();
        handler.startElement(null, "array", "array", attributes);
        for(Iterator iter = list.iterator();iter.hasNext();) {
            serializeObject(iter.next(), handler);
        }
        handler.endElement(null, "array", "array");
    }

    /**
     * Creates an element with the specified tag name and character content.
     * @param tag tag name.
     * @param content character content.
     * @param handler destination of serialization events.
     * @throws SAXException if exception during serialization.
     */
    private static void serializeElement(final String tag,
                                  final String content,
                                  final ContentHandler handler)
         throws SAXException {
        AttributesImpl attributes = new AttributesImpl();
        handler.startElement(null, tag, tag, attributes);
        handler.characters(content.toCharArray(), 0, content.length());
        handler.endElement(null, tag, tag);
    }


    /**
     * Serialize a Number as an integer element.
     * @param integer number to serialize.
     * @param handler destination of serialization events.
     * @throws SAXException if exception during serialization.
     */
    private static void serializeInteger(final Number integer,
                                         final ContentHandler handler)
         throws SAXException {
        serializeElement("integer", String.valueOf(integer.longValue()),
                handler);
    }

    /**
     * Serialize a Number as a real element.
     * @param real number to serialize.
     * @param handler destination of serialization events.
     * @throws SAXException if exception during serialization.
     */
    private static void serializeReal(final Number real,
                                      final ContentHandler handler)
         throws SAXException {
        serializeElement("real",
                String.valueOf(real.doubleValue()),
                handler);
    }


    /**
     * Serialize a Boolean as a true or false element.
     * @param val boolean to serialize.
     * @param handler destination of serialization events.
     * @throws SAXException if exception during serialization.
     */
    private static void serializeBoolean(final Boolean val,
                                         final ContentHandler handler)
         throws SAXException {
        String tag = "false";
        if (val.booleanValue()) {
            tag = "true";
        }
        AttributesImpl attributes = new AttributesImpl();
        handler.startElement(null, tag, tag, attributes);
        handler.endElement(null, tag, tag);
    }

    /**
     * Serialize a string as a string element.
     * @param val string to serialize.
     * @param handler destination of serialization events.
     * @throws SAXException if exception during serialization.
     */
    private static void serializeString(final String val,
                                        final ContentHandler handler)
         throws SAXException {
        serializeElement("string",
                val,
                handler);
    }


    /**
     * Serialize an object using the best available element.
     * @param obj object to serialize.
     * @param handler destination of serialization events.
     * @throws SAXException if exception during serialization.
     */
    private static void serializeObject(final Object obj,
                                        final ContentHandler handler)
        throws SAXException {
        if (obj instanceof Map) {
            serializeMap((Map) obj, handler);
        } else if (obj instanceof List) {
            serializeList((List) obj, handler);      
        } else if (obj instanceof Number) {
            if(obj instanceof Double
                ||obj instanceof Float) {
                serializeReal((Number) obj, handler);
            } else {
                serializeInteger((Number) obj, handler);
            }
        } else if (obj instanceof Boolean) {
            serializeBoolean((Boolean) obj, handler);
        } else {
            serializeString(String.valueOf(obj), handler);
        }
    }
}
