/*
   Copyright 2007 Attila Szegedi

   Licensed 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.dynalang.mop.beans;

import java.beans.IntrospectionException;
import java.lang.reflect.Array;
import java.util.Collections;
import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.Set;

import org.dynalang.mop.CallProtocol;

/**
 * Adds access to array elements by numeric index as well as the "length" 
 * read-only property to {@link BeanMetaobjectProtocol}.
 * @author Attila Szegedi

 * @version $Id: $
 * @param <T> the class of the array
 */
public class ArrayMetaobjectProtocol<T extends Object> extends BeanMetaobjectProtocol<T> {

    private final Class<?> componentType;
    
    /**
     * Constructs a new metaobject protocol for objects of a certain array class.
     * @param clazz the class to construct a metaobject protocol for
     * @param methodsEnumerable if true, methods are enumerable through the
     * {@link #properties(Object)} and {@link #propertyIds(Object)}.
     * @throws IllegalArgumentException if the passed class is not an array
     * class.
     * @throws IntrospectionException
     */
    public ArrayMetaobjectProtocol(Class<T> clazz, boolean methodsEnumerable) throws IntrospectionException {
	super(clazz, methodsEnumerable);
	if(!clazz.isArray()) {
	    throw new IllegalArgumentException("Class " + clazz.getName() + 
		    " is not an array class");
	}
	componentType = clazz.getComponentType();
    }

    @Override
    public Object get(Object target, Object propertyId) {
        if(propertyId instanceof Number) {
            return get(target, ((Number)propertyId).longValue());
        }
        if("length".equals(propertyId)) {
            return Integer.valueOf(Array.getLength(target));
        }
        return super.get(target, propertyId);
    }
    
    @Override
    public Object get(Object target, long propertyId) {
        return isWithinBounds(target, propertyId) ? Array.get(target, (int)propertyId) : Result.doesNotExist;
    }

    @Override
    public Boolean has(Object target, Object propertyId) {
        if(propertyId instanceof Number) {
            return has(target, ((Number)propertyId).longValue());
        }
        if("length".equals(propertyId)) {
            return Boolean.TRUE;
        }
        return super.has(target, propertyId);
    }
    
    @Override
    public Boolean has(Object target, long propertyId) {
        return Boolean.valueOf(isWithinBounds(target, propertyId));
    }

    private static enum PropertyIterationPhase {
	array,
	length,
	methods
    }
    
    private static final Set<Object> lengthSet = Collections.singleton((Object)"length");
    
    private Iterator<? extends Object> getSuperPropertyIds(Object target) {
	return super.propertyIds(target);
    }
    
    @Override
    public Iterator<Object> propertyIds(final Object target) {
        return new Iterator<Object>() {
            private Iterator<? extends Object> curr = new Iterator<Integer>() {
        	private int i = 0;
        	private final int l = Array.getLength(target);
        	
        	public boolean hasNext() {
        	    return i < l;
        	}
        	
        	public Integer next() {
        	    if(i < l) {
        		return Integer.valueOf(i++);
        	    }
        	    throw new NoSuchElementException();
        	}
        	
        	public void remove() {
        	    throw new UnsupportedOperationException();
        	}
        	
            };
            private PropertyIterationPhase phase = PropertyIterationPhase.array;
            
            public boolean hasNext() {
        	for(;;) {
                    if(curr.hasNext()) {
                        return true;
                    }
                    if(!switchState()) {
                	return false;
                    }
        	}
            }

            private boolean switchState() {
                switch(phase) {
                    case array: {
                        phase = PropertyIterationPhase.length;
                        curr = lengthSet.iterator();
                        return true;
                    }
                    case length: {
                        phase = PropertyIterationPhase.methods;
                        curr = getSuperPropertyIds(target);
                        return true;
                    }
                    case methods: {
                        return false;
                    }
                    default: {
                	throw new AssertionError();
                    }
                }
            }
            
            public Object next() {
        	for(;;) {
                    try {
                	return curr.next();
                    }
                    catch(NoSuchElementException e) {
                	if(!switchState()) {
                	    throw e;
                	}
                    }
        	}
            }
            
            public void remove() {
        	throw new UnsupportedOperationException();
            }
        };
    }
    
    @Override
    public Result put(Object target, Object propertyId, Object value, CallProtocol callProtocol) {
        if(propertyId instanceof Number) {
            return put(target, ((Number)propertyId).longValue(), value, callProtocol);
        }
        if("length".equals(propertyId)) {
            return Result.notWritable;
        }
        return super.put(target, propertyId, value, callProtocol);
    }
    
    @Override
    public Result put(Object target, long propertyId, Object value, CallProtocol callProtocol) {
	if(isWithinBounds(target, propertyId)) {
	    value = callProtocol.representAs(value, componentType);
	    if(value == Result.noAuthority || value == Result.noRepresentation) {
		return Result.noRepresentation;
	    }
	    Array.set(target, (int)propertyId, value);
	    return Result.ok;
	}
	return Result.doesNotExist;
    }
    
    private static boolean isWithinBounds(Object target, long propertyId) {
	return 0 <= propertyId && propertyId < Array.getLength(target);
    }
}
