/*
 * @author Ranides Atterwim <ranides@gmail.com>
 * @copyright Ranides Atterwim
 * @license WTFPL
 * @url http://ranides.net/projects/assira
 */

package net.ranides.assira.reflection.impl.bean;

import java.util.List;
import net.ranides.assira.collection.arrays.NativeArray;
import net.ranides.assira.collection.arrays.NativeArrayAccess;
import net.ranides.assira.collection.arrays.NativeArrayUtils;
import net.ranides.assira.collection.lists.VirtualList;
import net.ranides.assira.functional.VarFunction;
import net.ranides.assira.reflection.IClass;
import net.ranides.assira.reflection.IMethod;
import net.ranides.assira.reflection.util.ClassUtils;

/**
 * Represents indexed bean property as List. "value" getter & setter should return array.
 * Optional "index" getter & setter should return/take type identical to array component.
 * 
 * #set unwraps passed list to array and use "value setter"
 * 
 * #get returns VirtualList:
 *      list contains "cached" data obtained by "value getter"
 *      #get(index) uses "index getter" if declared, or cached value otherwise 
 *      #set(index) changes content of virtual list 
 *      #set(index) use "index setter" if declared to modify bean's content instantly 
 *      #size() returns size of cached data 
 * 
 * Please note that if "index setter" isn't declared, you have to manually return back modified list: 
 *      List<Object> list = property.get(bean)
 *      list.set ...
 *      property.set(list)
 * 
 * Please note that structural changes have to be manually returned back even if you declared "index setter":
 *      List<Object> list = property.get(bean)
 *      list.remove ...
 *      list.add ...
 *      property.set(list)
 * 
 */
public class RPArray extends ABeanProperty {
    
    private final Class<?> component;
    private final NativeArrayAccess access;
    
    private final IMethod vgetter;
    private final IMethod vsetter;
    private final IMethod igetter;
    private final IMethod isetter;

    public RPArray(RBeanPropertyInfo info) {
        super(itype(info), info.name());
        this.component = info.item().raw();
        this.access = NativeArrayAccess.forComponent(component);
        
        this.vgetter = info.vgetter();
        this.vsetter = info.vsetter();
        this.igetter = info.igetter();
        this.isetter = info.isetter();
    }
    
    private static IClass<?> itype(RBeanPropertyInfo info) {
        return IClass.generic(List.class, ClassUtils.box(info.item()));
    }
        
    @Override
    public void set(Object that, Object value) {
        checkWA();
        if(null == value) {
            vsetter.apply(that, new Object[]{null});
        } else {
            List<?> source = (List<?>)value;
            NativeArray target = NativeArray.allocate(component, source.size());
            NativeArrayUtils.collect(source.iterator(), target);
            vsetter.apply(that, new Object[]{target.$array()});
        }
    }

    @Override
    public Object get(Object that) {
        checkRA();
        Object array = vgetter.apply(that);
        return array!=null ? new ArrayWrapper(this, that, array) : null;
    }

    @Override
    public boolean isReadable() {
        return vgetter != null;
    }

    @Override
    public boolean isWritable() {
        return vsetter != null;
    }
    
    private void checkRA() {
        if(!isReadable()) {
            throw new UnsupportedOperationException("property " + name() + " is read-only");
        }
    }
    
    private void checkWA() {
        if(!isWritable()) {
            throw new UnsupportedOperationException("property " + name() + " is write-only");
        }
    }
    
    private static final class ArrayWrapper extends VirtualList<Object> {
        
        private static final long serialVersionUID = 1L;

        private final NativeArray data;
        private final VarFunction mset;
        private final VarFunction mget;

        @SuppressWarnings("unchecked")
        public ArrayWrapper(RPArray rpa, Object that, Object array) {
            this.data = rpa.access.bind(array);
            this.mget = null != rpa.igetter ? rpa.igetter.bind(that) : null;
            this.mset = null != rpa.isetter ? rpa.isetter.bind(that) : null;
        }

        @Override
        public int size() {
            return data.size();
        }

        @Override
        public Object get(int index) {
            if(null != mget) {
                return mget.call(index);
            } else {
                return data.get(index);
            }
        }

        @Override
        public Object set(int index, Object element) {
            Object prv = data.replace(index, element);
            if(null != mset) {
                prv = mset.call(index, element);
            }
            return prv;
        }

    }
    
}
