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

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import net.ranides.assira.collection.CollectionUtils;
import net.ranides.assira.collection.list.VirtualList;
import net.ranides.assira.collection.map.Cache;
import net.ranides.assira.generic.Function;
import net.ranides.assira.reflection.GenericClass;
import net.ranides.assira.reflection.TypeFactory;
import net.ranides.assira.reflection.InspectException;
import net.ranides.assira.reflection.util.MemberUtils;
import net.ranides.assira.reflection.util.ReflectUtils;
import net.ranides.assira.reflection.util.BeanUtils;
import net.ranides.assira.reflection.util.MethodUtils;
import net.ranides.assira.text.StrBuilder;

/**
 *
 * @author ranides
 */
public class BeanInspector {
    
    private static final BeanInspector EMPTY = new BeanInspector();
    
    private static final Cache<Class<?>, BeanInspector> CACHE = new Cache<>();
    
    private final Class<?> type;
    private final Map<String, Method> getters;
    private final Map<String, Method> setters;
    private final Map<String, Field> fields;
    
    private BeanInspector() {
        this.type = Void.class;
        this.getters = Collections.emptyMap();
        this.setters = Collections.emptyMap();
        this.fields = Collections.emptyMap();
    }
    
    private BeanInspector(Class<?> type) {
        this.type = type;
        this.getters = new HashMap<>();
        this.setters = new HashMap<>();
        this.fields = new HashMap<>();
        
        for(Method method : MethodInspector.inclass(type).follow().declared().reflective()) {
            if( !MemberUtils.isPublic(method) ) {
                continue;
            }
            if(MemberUtils.isGetter(method) ) {
                getters.put(BeanUtils.$asPropertyName(method), ReflectUtils.access(method));
                continue;
            }
            if(MemberUtils.isSetter(method) ) {
                setters.put(BeanUtils.$asPropertyName(method), ReflectUtils.access(method));
                continue;
            }
        }
        for(Field field : type.getFields()) {
            fields.put(field.getName(), ReflectUtils.access(field));
        }
    }
    
    public static BeanInspector forObject(Object object) {
        if(null==object) {
            return EMPTY;
        }
        return forClass(object.getClass());
    }
    
    public static BeanInspector forClass(Class<?> clazz) {
        if(null==clazz || Void.class.equals(clazz) || void.class.equals(clazz)) {
            return EMPTY;
        }
        if(!CACHE.containsKey(clazz)) {
            CACHE.put(clazz, new BeanInspector(clazz));
        }
        return CACHE.get(clazz);
    }
    
    public Class<?> type() {
        return type;
    }

    public GenericClass typeof(String name) throws InspectException {
        if( getters.containsKey(name) ) {
            return MethodUtils.typeof(getters.get(name));
        }
        if( setters.containsKey(name) ) {
            return MethodUtils.params(setters.get(name)).get(0).type();
        }
        if( fields.containsKey(name) ) {
            return TypeFactory.construct(fields.get(name).getGenericType());
        }
        throw reportNFound(name);
    }
    
    public Set<String> properties() {
        Set<String> ret = new HashSet<>();
        ret.addAll(getters.keySet());
        ret.addAll(setters.keySet());
        ret.addAll(fields.keySet());
        return ret;
    }
    
    public boolean isReadable(String name) {
        return getters.containsKey(name) || fields.containsKey(name);
    }
    
    public boolean isWritable(String name) {
        return setters.containsKey(name) || (fields.containsKey(name) && !MemberUtils.isFinal(fields.get(name)));
    }
    
    public Object get(Object object, String name) {
        try {
            if(getters.containsKey(name)) {
                return getters.get(name).invoke(object);
            }
            if(fields.containsKey(name)) {
                return fields.get(name).get(object);
            }
        } catch (IllegalArgumentException | ReflectiveOperationException cause) {
            throw new InspectException(cause);
        } 
        throw reportNFound(name);
    }
    
    public void put(Object object, String name, Object value) {
        try {
            if(setters.containsKey(name)) {
                setters.get(name).invoke(object, value);
                return;
            }
            if(fields.containsKey(name)) {
                fields.get(name).set(object, value);
                return;
            }
        } catch (IllegalArgumentException | ReflectiveOperationException cause) {
            throw new InspectException(cause);
        } 
        throw reportNFound(name);
    }
    
    public List<Object> each(List<?> objects, String name) {
        return new EachList(objects, name);
    }
    
    public Iterable<Object> each(Iterable<?> objects, String name) {
        return new EachCollection(objects, name);
    }
    
    private InspectException reportNFound(String name) {
        return new InspectException("class " + type.getName() + " has not property: " + name);
    }
    
    private class EachList extends VirtualList<Object> {
        protected final List<?> content;
        protected final String name;

        public EachList(List<?> content, String name) {
            this.content = content;
            this.name = name;
        }
        @Override
        public Object get(int index) {
            return BeanInspector.this.get(content.get(index), name);
        }
        @Override
        public int size() {
            return content.size();
        }
        @Override
        public Object set(int index, Object value) {
            Object prev = BeanInspector.this.get(content.get(index), name);
            BeanInspector.this.put(content.get(index), name, value);
            return prev;
        }
    }

    private class EachCollection implements Iterable<Object>, Function<Object, Object> {
        protected final Iterable<Object> collection;
        protected final String name;

        @SuppressWarnings("unchecked")
        public EachCollection(Iterable<?> collection, String name) {
            this.collection = (Iterable<Object>) collection;
            this.name = name;
        }
        @Override
        public Iterator<Object> iterator() {
            return CollectionUtils.adapt(collection.iterator(), this);
        }
        @Override
        public Object apply(Object source) {
            return BeanInspector.this.get(source, name);
        }

        @Override
        public String toString() {
            StrBuilder builder = new StrBuilder()
                .append("{").list(",").setNullText("null");
            for(Object value : this) {
                builder.item().append(value);
            }
            builder.append("}");
            return builder.toString();
        }
        
        
        
    }

}
