/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF 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.apache.cxf.aegis.type.basic;

import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import javax.xml.namespace.QName;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.cxf.aegis.Context;
import org.apache.cxf.aegis.DatabindingException;
import org.apache.cxf.aegis.type.Type;
import org.apache.cxf.aegis.type.TypeUtil;
import org.apache.cxf.aegis.util.NamespaceHelper;
import org.apache.cxf.aegis.xml.MessageReader;
import org.apache.cxf.aegis.xml.MessageWriter;
import org.apache.cxf.common.util.SOAPConstants;
import org.apache.cxf.common.xmlschema.XmlSchemaConstants;
import org.jdom.Attribute;
import org.jdom.Element;
import org.jdom.Namespace;

/**
 * An ArrayType.
 * 
 * @author <a href="mailto:dan@envoisolutions.com">Dan Diephouse</a>
 */
public class ArrayType extends Type {
    private static final Log LOG = LogFactory.getLog(ArrayType.class);

    private QName componentName;
    private long minOccurs;
    private long maxOccurs = Long.MAX_VALUE;
    private boolean flat;

    public ArrayType() {
    }

    @Override
    public Object readObject(MessageReader reader, Context context) throws DatabindingException {
        try {
            Collection values = readCollection(reader, context);

            return makeArray(getComponentType().getTypeClass(), values);
        } catch (IllegalArgumentException e) {
            throw new DatabindingException("Illegal argument.", e);
        }
    }

    protected Collection<Object> createCollection() {
        return new ArrayList<Object>();
    }

    protected Collection readCollection(MessageReader reader, Context context) throws DatabindingException {
        Collection<Object> values = createCollection();

        while (reader.hasMoreElementReaders()) {
            MessageReader creader = reader.getNextElementReader();
            Type compType = TypeUtil.getReadType(creader.getXMLStreamReader(), context.getGlobalContext(),
                                                 getComponentType());

            if (creader.isXsiNil()) {
                values.add(null);
                creader.readToEnd();
            } else {
                values.add(compType.readObject(creader, context));
            }

            // check max occurs
            int size = values.size();
            if (size > maxOccurs) {
                throw new DatabindingException("The number of elements in " + getSchemaType()
                                               + " exceeds the maximum of " + maxOccurs);
            }

        }

        // check min occurs
        if (values.size() < minOccurs) {
            throw new DatabindingException("The number of elements in " + getSchemaType()
                                           + " does not meet the minimum of " + minOccurs);
        }
        return values;
    }

    @SuppressWarnings("unchecked")
    protected Object makeArray(Class arrayType, Collection values) {
        int i;
        int n;
        Object array = null;
        if (Integer.TYPE.equals(arrayType)) {
            Object[] objects = values.toArray();
            array = Array.newInstance(Integer.TYPE, objects.length);
            for (i = 0, n = objects.length; i < n; i++) {
                Array.set(array, i, objects[i]);
            }
        } else if (Long.TYPE.equals(arrayType)) {
            Object[] objects = values.toArray();
            array = Array.newInstance(Long.TYPE, objects.length);
            for (i = 0, n = objects.length; i < n; i++) {
                Array.set(array, i, objects[i]);
            }
        } else if (Short.TYPE.equals(arrayType)) {
            Object[] objects = values.toArray();
            array = Array.newInstance(Short.TYPE, objects.length);
            for (i = 0, n = objects.length; i < n; i++) {
                Array.set(array, i, objects[i]);
            }
        } else if (Double.TYPE.equals(arrayType)) {
            Object[] objects = values.toArray();
            array = Array.newInstance(Double.TYPE, objects.length);
            for (i = 0, n = objects.length; i < n; i++) {
                Array.set(array, i, objects[i]);
            }
        } else if (Float.TYPE.equals(arrayType)) {
            Object[] objects = values.toArray();
            array = Array.newInstance(Float.TYPE, objects.length);
            for (i = 0, n = objects.length; i < n; i++) {
                Array.set(array, i, objects[i]);
            }
        } else if (Byte.TYPE.equals(arrayType)) {
            Object[] objects = values.toArray();
            array = Array.newInstance(Byte.TYPE, objects.length);
            for (i = 0, n = objects.length; i < n; i++) {
                Array.set(array, i, objects[i]);
            }
        } else if (Boolean.TYPE.equals(arrayType)) {
            Object[] objects = values.toArray();
            array = Array.newInstance(Boolean.TYPE, objects.length);
            for (i = 0, n = objects.length; i < n; i++) {
                Array.set(array, i, objects[i]);
            }
        } else if (Character.TYPE.equals(arrayType)) {
            Object[] objects = values.toArray();
            array = Array.newInstance(Character.TYPE, objects.length);
            for (i = 0, n = objects.length; i < n; i++) {
                Array.set(array, i, objects[i]);
            }
        }
        return array == null ? values.toArray((Object[])Array.newInstance(getComponentType().getTypeClass(),
                                                                          values.size())) : array;
    }

    @Override
    public void writeObject(Object values, MessageWriter writer, Context context) 
        throws DatabindingException {
        boolean forceXsiWrite = false;
        if (values == null) {
            return;
        }

        Type type = getComponentType();
        if (type == null) {
            throw new DatabindingException("Couldn't find type for array.");
        }
        if (XmlSchemaConstants.ANY_TYPE_QNAME.equals(type.getSchemaType())) {
            forceXsiWrite = true;
        }

        String ns = null;
        if (type.isAbstract()) {
            ns = getSchemaType().getNamespaceURI();
        } else {
            ns = type.getSchemaType().getNamespaceURI();
        }

        String name = type.getSchemaType().getLocalPart();


        Class arrayType = type.getTypeClass();

        boolean oldXsiWrite = context.getGlobalContext().isWriteXsiTypes();
        try {
            if (forceXsiWrite) {
                context.getGlobalContext().setWriteXsiTypes(true);
            }

            int i;
            int n;
            if (Object.class.isAssignableFrom(arrayType)) {
                Object[] objects = (Object[])values;
                for (i = 0, n = objects.length; i < n; i++) {
                    writeValue(objects[i], writer, context, type, name, ns);
                }
            } else if (Integer.TYPE.equals(arrayType)) {
                int[] objects = (int[])values;
                for (i = 0, n = objects.length; i < n; i++) {
                    writeValue(new Integer(objects[i]), writer, context, type, name, ns);
                }
            } else if (Long.TYPE.equals(arrayType)) {
                long[] objects = (long[])values;
                for (i = 0, n = objects.length; i < n; i++) {
                    writeValue(new Long(objects[i]), writer, context, type, name, ns);
                }
            } else if (Short.TYPE.equals(arrayType)) {
                short[] objects = (short[])values;
                for (i = 0, n = objects.length; i < n; i++) {
                    writeValue(new Short(objects[i]), writer, context, type, name, ns);
                }
            } else if (Double.TYPE.equals(arrayType)) {
                double[] objects = (double[])values;
                for (i = 0, n = objects.length; i < n; i++) {
                    writeValue(new Double(objects[i]), writer, context, type, name, ns);
                }
            } else if (Float.TYPE.equals(arrayType)) {
                float[] objects = (float[])values;
                for (i = 0, n = objects.length; i < n; i++) {
                    writeValue(new Float(objects[i]), writer, context, type, name, ns);
                }
            } else if (Byte.TYPE.equals(arrayType)) {
                byte[] objects = (byte[])values;
                for (i = 0, n = objects.length; i < n; i++) {
                    writeValue(new Byte(objects[i]), writer, context, type, name, ns);
                }
            } else if (Boolean.TYPE.equals(arrayType)) {
                boolean[] objects = (boolean[])values;
                for (i = 0, n = objects.length; i < n; i++) {
                    writeValue(Boolean.valueOf(objects[i]), writer, context, type, name, ns);
                }
            } else if (Character.TYPE.equals(arrayType)) {
                char[] objects = (char[])values;
                for (i = 0, n = objects.length; i < n; i++) {
                    writeValue(new Character(objects[i]), writer, context, type, name, ns);
                }
            }
        } finally {
            context.getGlobalContext().setWriteXsiTypes(oldXsiWrite);
        }
    }

    protected void writeValue(Object value, MessageWriter writer, Context context, Type type, String name,
                              String ns) throws DatabindingException {
        type = TypeUtil.getWriteType(context.getGlobalContext(), value, type);
        MessageWriter cwriter;
        if (type.isWriteOuter()) {
            cwriter = writer.getElementWriter(name, ns);
        } else {
            cwriter = writer;
        }

        if (value == null && type.isNillable()) {
            cwriter.writeXsiNil();
        } else {
            type.writeObject(value, cwriter, context);
        }
        
        if (type.isWriteOuter()) {
            cwriter.close();
        }
    }

    @Override
    public void writeSchema(Element root) {
        try {
            if (hasDefinedArray(root)) {
                return;
            }

            Element complex = new Element("complexType", SOAPConstants.XSD_PREFIX, SOAPConstants.XSD);
            complex.setAttribute(new Attribute("name", getSchemaType().getLocalPart()));
            root.addContent(complex);

            Element seq = new Element("sequence", SOAPConstants.XSD_PREFIX, SOAPConstants.XSD);
            complex.addContent(seq);

            Element element = new Element("element", SOAPConstants.XSD_PREFIX, SOAPConstants.XSD);
            seq.addContent(element);

            Type componentType = getComponentType();
            String prefix = NamespaceHelper.getUniquePrefix(root, componentType.getSchemaType()
                .getNamespaceURI());

            element.setAttribute(new Attribute("name", componentType.getSchemaType().getLocalPart()));
            element.setAttribute(TypeUtil.createTypeAttribute(prefix, componentType, root));

            if (componentType.isNillable()) {
                element.setAttribute(new Attribute("nillable", "true"));
            }

            element.setAttribute(new Attribute("minOccurs", Long.valueOf(getMinOccurs()).toString()));

            if (maxOccurs == Long.MAX_VALUE) {
                element.setAttribute(new Attribute("maxOccurs", "unbounded"));
            } else {
                element.setAttribute(new Attribute("maxOccurs", Long.valueOf(getMaxOccurs()).toString()));
            }

        } catch (IllegalArgumentException e) {
            throw new DatabindingException("Illegal argument.", e);
        }
    }

    /**
     * Since both an Array and a List can have the same type definition, double
     * check that there isn't already a defined type already.
     * 
     * @param root
     * @return
     */
    private boolean hasDefinedArray(Element root) {
        List children = root.getChildren("complexType", Namespace.getNamespace(SOAPConstants.XSD));
        for (Iterator itr = children.iterator(); itr.hasNext();) {
            Element e = (Element)itr.next();

            if (e.getAttributeValue("name").equals(getSchemaType().getLocalPart())) {
                return true;
            }
        }
        return false;
    }

    /**
     * We need to write a complex type schema for Beans, so return true.
     * 
     * @see org.apache.cxf.aegis.type.Type#isComplex()
     */
    @Override
    public boolean isComplex() {
        return true;
    }

    public QName getComponentName() {
        return componentName;
    }

    public void setComponentName(QName componentName) {
        this.componentName = componentName;
    }

    /**
     * @see org.apache.cxf.aegis.type.Type#getDependencies()
     */
    @Override
    public Set<Type> getDependencies() {
        Set<Type> deps = new HashSet<Type>();

        deps.add(getComponentType());

        return deps;
    }

    /**
     * Get the <code>Type</code> of the elements in the array.
     * 
     * @return
     */
    public Type getComponentType() {
        Class compType = getTypeClass().getComponentType();

        Type type;

        if (componentName == null) {
            type = getTypeMapping().getType(compType);
        } else {
            type = getTypeMapping().getType(componentName);

            // We couldn't find the type the user specified. One is created
            // below instead.
            if (type == null) {
                LOG.debug("Couldn't find array component type " + componentName + ". Creating one instead.");
            }
        }

        if (type == null) {
            type = getTypeMapping().getTypeCreator().createType(compType);
            getTypeMapping().register(type);
        }

        return type;
    }

    public long getMaxOccurs() {
        return maxOccurs;
    }

    public void setMaxOccurs(long maxOccurs) {
        this.maxOccurs = maxOccurs;
    }

    public long getMinOccurs() {
        return minOccurs;
    }

    public void setMinOccurs(long minOccurs) {
        this.minOccurs = minOccurs;
    }

    public boolean isFlat() {
        return flat;
    }

    public void setFlat(boolean flat) {
        setWriteOuter(!flat);
        this.flat = flat;
    }
}
