/*
 * 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 freemarker.template;

import java.io.Serializable;
import java.lang.reflect.Array;

import freemarker.ext.util.WrapperTemplateModel;

/**
 * Adapts an {@code array} of a non-primitive elements to the corresponding {@link TemplateModel} interface(s), most
 * importantly to {@link TemplateHashModelEx}. If you aren't wrapping an already existing {@code array}, but build a
 * sequence specifically to be used from a template, also consider using {@link SimpleSequence} (see comparison there).
 *
 * <p>
 * Thread safety: A {@link DefaultListAdapter} is as thread-safe as the array that it wraps is. Normally you only
 * have to consider read-only access, as the FreeMarker template language doesn't allow writing these sequences (though
 * of course, Java methods called from the template can violate this rule).
 * 
 * <p>
 * This adapter is used by {@link DefaultObjectWrapper} if its {@code useAdaptersForCollections} property is
 * {@code true}, which is the default when its {@code incompatibleImprovements} property is 2.3.22 or higher.
 * 
 * @see SimpleSequence
 * @see DefaultListAdapter
 * @see TemplateSequenceModel
 * 
 * @since 2.3.22
 */
public abstract class DefaultArrayAdapter extends WrappingTemplateModel implements TemplateSequenceModel,
        AdapterTemplateModel, WrapperTemplateModel, Serializable {

    /**
     * Factory method for creating new adapter instances.
     * 
     * @param array
     *            The array to adapt; can't be {@code null}. Must be an array. 
     * @param wrapper
     *            The {@link ObjectWrapper} used to wrap the items in the array. Has to be
     *            {@link ObjectWrapperAndUnwrapper} because of planned future features.
     */
    public static DefaultArrayAdapter adapt(Object array, ObjectWrapperAndUnwrapper wrapper) {
        final Class componentType = array.getClass().getComponentType();
        if (componentType == null) {
            throw new IllegalArgumentException("Not an array");
        }
        
        if (componentType.isPrimitive()) {
            if (componentType == int.class) {
                return new IntArrayAdapter((int[]) array, wrapper);
            }
            if (componentType == double.class) {
                return new DoubleArrayAdapter((double[]) array, wrapper);
            }
            if (componentType == long.class) {
                return new LongArrayAdapter((long[]) array, wrapper);
            }
            if (componentType == boolean.class) {
                return new BooleanArrayAdapter((boolean[]) array, wrapper);
            }
            if (componentType == float.class) {
                return new FloatArrayAdapter((float[]) array, wrapper);
            }
            if (componentType == char.class) {
                return new CharArrayAdapter((char[]) array, wrapper);
            }
            if (componentType == short.class) {
                return new ShortArrayAdapter((short[]) array, wrapper);
            }
            if (componentType == byte.class) {
                return new ByteArrayAdapter((byte[]) array, wrapper);
            }
            return new GenericPrimitiveArrayAdapter(array, wrapper);
        } else {
            return new ObjectArrayAdapter((Object[]) array, wrapper);
        }
    }

    private DefaultArrayAdapter(ObjectWrapper wrapper) {
        super(wrapper);
    }

    @Override
    public final Object getAdaptedObject(Class hint) {
        return getWrappedObject();
    }

    private static class ObjectArrayAdapter extends DefaultArrayAdapter {

        private final Object[] array;

        private ObjectArrayAdapter(Object[] array, ObjectWrapper wrapper) {
            super(wrapper);
            this.array = array;
        }

        @Override
        public TemplateModel get(int index) throws TemplateModelException {
            return index >= 0 && index < array.length ? wrap(array[index]) : null;
        }

        @Override
        public int size() throws TemplateModelException {
            return array.length;
        }

        @Override
        public Object getWrappedObject() {
            return array;
        }

    }

    private static class ByteArrayAdapter extends DefaultArrayAdapter {

        private final byte[] array;

        private ByteArrayAdapter(byte[] array, ObjectWrapper wrapper) {
            super(wrapper);
            this.array = array;
        }

        @Override
        public TemplateModel get(int index) throws TemplateModelException {
            return index >= 0 && index < array.length ? wrap(Byte.valueOf(array[index])) : null;
        }

        @Override
        public int size() throws TemplateModelException {
            return array.length;
        }

        @Override
        public Object getWrappedObject() {
            return array;
        }

    }

    private static class ShortArrayAdapter extends DefaultArrayAdapter {

        private final short[] array;

        private ShortArrayAdapter(short[] array, ObjectWrapper wrapper) {
            super(wrapper);
            this.array = array;
        }

        @Override
        public TemplateModel get(int index) throws TemplateModelException {
            return index >= 0 && index < array.length ? wrap(Short.valueOf(array[index])) : null;
        }

        @Override
        public int size() throws TemplateModelException {
            return array.length;
        }

        @Override
        public Object getWrappedObject() {
            return array;
        }

    }

    private static class IntArrayAdapter extends DefaultArrayAdapter {

        private final int[] array;

        private IntArrayAdapter(int[] array, ObjectWrapper wrapper) {
            super(wrapper);
            this.array = array;
        }

        @Override
        public TemplateModel get(int index) throws TemplateModelException {
            return index >= 0 && index < array.length ? wrap(Integer.valueOf(array[index])) : null;
        }

        @Override
        public int size() throws TemplateModelException {
            return array.length;
        }

        @Override
        public Object getWrappedObject() {
            return array;
        }

    }

    private static class LongArrayAdapter extends DefaultArrayAdapter {

        private final long[] array;

        private LongArrayAdapter(long[] array, ObjectWrapper wrapper) {
            super(wrapper);
            this.array = array;
        }

        @Override
        public TemplateModel get(int index) throws TemplateModelException {
            return index >= 0 && index < array.length ? wrap(Long.valueOf(array[index])) : null;
        }

        @Override
        public int size() throws TemplateModelException {
            return array.length;
        }

        @Override
        public Object getWrappedObject() {
            return array;
        }

    }

    private static class FloatArrayAdapter extends DefaultArrayAdapter {

        private final float[] array;

        private FloatArrayAdapter(float[] array, ObjectWrapper wrapper) {
            super(wrapper);
            this.array = array;
        }

        @Override
        public TemplateModel get(int index) throws TemplateModelException {
            return index >= 0 && index < array.length ? wrap(Float.valueOf(array[index])) : null;
        }

        @Override
        public int size() throws TemplateModelException {
            return array.length;
        }

        @Override
        public Object getWrappedObject() {
            return array;
        }

    }

    private static class DoubleArrayAdapter extends DefaultArrayAdapter {

        private final double[] array;

        private DoubleArrayAdapter(double[] array, ObjectWrapper wrapper) {
            super(wrapper);
            this.array = array;
        }

        @Override
        public TemplateModel get(int index) throws TemplateModelException {
            return index >= 0 && index < array.length ? wrap(Double.valueOf(array[index])) : null;
        }

        @Override
        public int size() throws TemplateModelException {
            return array.length;
        }

        @Override
        public Object getWrappedObject() {
            return array;
        }

    }

    private static class CharArrayAdapter extends DefaultArrayAdapter {

        private final char[] array;

        private CharArrayAdapter(char[] array, ObjectWrapper wrapper) {
            super(wrapper);
            this.array = array;
        }

        @Override
        public TemplateModel get(int index) throws TemplateModelException {
            return index >= 0 && index < array.length ? wrap(Character.valueOf(array[index])) : null;
        }

        @Override
        public int size() throws TemplateModelException {
            return array.length;
        }

        @Override
        public Object getWrappedObject() {
            return array;
        }

    }

    private static class BooleanArrayAdapter extends DefaultArrayAdapter {

        private final boolean[] array;

        private BooleanArrayAdapter(boolean[] array, ObjectWrapper wrapper) {
            super(wrapper);
            this.array = array;
        }

        @Override
        public TemplateModel get(int index) throws TemplateModelException {
            return index >= 0 && index < array.length ? wrap(Boolean.valueOf(array[index])) : null;
        }

        @Override
        public int size() throws TemplateModelException {
            return array.length;
        }

        @Override
        public Object getWrappedObject() {
            return array;
        }

    }

    /**
     * Much slower than the specialized versions; used only as the last resort.
     */
    private static class GenericPrimitiveArrayAdapter extends DefaultArrayAdapter {

        private final Object array;
        private final int length;

        private GenericPrimitiveArrayAdapter(Object array, ObjectWrapper wrapper) {
            super(wrapper);
            this.array = array;
            length = Array.getLength(array);
        }

        @Override
        public TemplateModel get(int index) throws TemplateModelException {
            return index >= 0 && index < length ? wrap(Array.get(array, index)) : null;
        }

        @Override
        public int size() throws TemplateModelException {
            return length;
        }

        @Override
        public Object getWrappedObject() {
            return array;
        }

    }

}
