/*
 * @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.*;
import java.util.Map.Entry;

import net.ranides.assira.collection.maps.HashMap;
import net.ranides.assira.collection.maps.HashMultiMap;
import net.ranides.assira.collection.maps.LazyMap;
import net.ranides.assira.collection.maps.MapUtils;
import net.ranides.assira.collection.maps.MultiMap;
import net.ranides.assira.collection.sets.HashSet;
import net.ranides.assira.reflection.BeanMethod;
import net.ranides.assira.reflection.BeanModel;
import net.ranides.assira.reflection.BeanProperty;
import net.ranides.assira.reflection.IArguments;
import net.ranides.assira.reflection.IAttribute;
import net.ranides.assira.reflection.IClass;
import net.ranides.assira.reflection.IMethod;
import net.ranides.assira.text.StringUtils;

/**
 *
 * @author Ranides Atterwim <ranides@gmail.com>
 */
public class RBeanModel implements BeanModel {
    
    private static final Set<String> OBJECT_METHODS = new HashSet<>(new String[]{"toString","hashCode","equals"});
    
    private static final IArguments INT_ARGS = IArguments.typeinfo(int.class);
    private static final IClass<?> INT_ARG = IClass.typeinfo(int.class);
    
    private final IClass<?> type;
    private final Map<String, BeanMethod> methods;
    private final Map<String, BeanProperty> properties;

    RBeanModel(IClass<?> type) {
        this.type = type;
        
        Map<String, RBeanPropertyInfo> pinfo = new LazyMap<>(new HashMap<>(), RBeanPropertyInfo::new);
        MultiMap<String, IMethod> minfo = new HashMultiMap<>();
                
        for(IMethod m : type.methods()) {
            if(m.parent().equals(IClass.OBJECT) && !OBJECT_METHODS.contains(m.name())) {
                continue;
            }
                
            if( isVGetter(m) ) {
                pinfo.get(asPropertyName(m.name())).vgetter(m);
            }
            else if( isIGetter(m) ) {
                pinfo.get(asPropertyName(m.name())).igetter(m);
            }
            else if( isVSetter(m) ) {
                pinfo.get(asPropertyName(m.name())).vsetter(m);
            }
            else if( isISetter(m) ) {
                pinfo.get(asPropertyName(m.name())).isetter(m);
            }
            else {
                minfo.put(m.name(), m);
            }
        }
        methods = new HashMap<>();
        for(String m : minfo.uniqueKeySet()) {
            methods.put(m, FBeanModel.resolve(minfo.getAll(m)));
        }
        properties = new HashMap<>();
        for(Entry<String,RBeanPropertyInfo> item : pinfo.entrySet()) {
            properties.put(item.getKey(), FBeanModel.resolve(item.getValue()));
        }
    }
    
    @Override
    public IClass<?> type() {
        return type;
    }

    @Override
    public Optional<BeanMethod> method(String name) {
        return Optional.ofNullable(methods.get(name));
    }

    @Override
    public Map<String, BeanMethod> methods() {
        return methods;
    }

    @Override
    public Optional<BeanProperty> property(String name) {
        return Optional.ofNullable(properties.get(name));
    }

    @Override
    public Map<String, BeanProperty> properties() {
        return properties;
    }

    @Override
    public Map<String, Object> values(Object that) {
        return MapUtils.wrap(properties, p -> p.get(that), (p,v) -> p.replace(that, v));
    }

    @Override
    public Object construct() {
        return type.construct();
    }

    @Override
    public Object construct(Map<String, Object> values) {
        Object o = type.construct();
        for(Entry<String, Object> p : values.entrySet()) {
            property(p.getKey()).get().set(o, p.getValue());
        }
        return o;
    }

    @Override
    public Object clone(Object that) {
        Object o = type.construct();
        for(BeanProperty p : properties.values()) {
            if(p.isReadable() && p.isWritable()) {
                p.set(o, p.get(that));
            }
        }
        return o;
    }
    
    private static boolean isVGetter(IMethod method) {
        // eliminacja na podstawie deklaracji
        if(IClass.OBJECT.equals(method.parent())) {
            return false;
        }
        if(!method.arguments().isEmpty()) {
            return false;
        }
        Set<IAttribute> ra = method.returns().attributes();
        if(ra.contains(IAttribute.VOID)) {
            return false;
        }
        
        String name = method.name();
        // akceptacja klasycznego gettera
        if(name.startsWith("get") && name.length()>3 && Character.isUpperCase(name.charAt(3)) ) {
            return true;
        }
        // akceptacja gettera logicznego "isValue"
        if(name.startsWith("is") && name.length()>2 && Character.isUpperCase(name.charAt(2)) && ra.contains(IAttribute.BOOLEAN) ) {
            return true;
        }
        return false;
    }
    
    private static boolean isIGetter(IMethod method) {
        // eliminacja na podstawie deklaracji
        if(IClass.OBJECT.equals(method.parent())) {
            return false;
        }
        if(!method.arguments().equals(INT_ARGS)) {
            return false;
        }
        Set<IAttribute> ra = method.returns().attributes();
        if(ra.contains(IAttribute.VOID)) {
            return false;
        }
        
        String name = method.name();
        // akceptacja klasycznego gettera
        if(name.startsWith("get") && name.length()>3 && Character.isUpperCase(name.charAt(3)) ) {
            return true;
        }
        // akceptacja gettera logicznego "isValue"
        if(name.startsWith("is") && name.length()>2 && Character.isUpperCase(name.charAt(2)) && ra.contains(IAttribute.BOOLEAN) ) {
            return true;
        }
        return false;
    }
    
    private static boolean isVSetter(IMethod method) {
        // eliminacja na podstawie deklaracji
        if(IClass.OBJECT.equals(method.parent())) {
            return false;
        }
        if(method.arguments().count()!=1) {
            return false;
        }
        if(!method.returns().attributes().contains(IAttribute.VOID) && !method.returns().equals(method.parent())) {
            return false;
        }

        String name = method.name();
        // akceptacja klasycznego settera
        if(name.startsWith("set") && name.length()>3 && Character.isUpperCase(name.charAt(3)) ) {
            return true;
        }
        return false;
    }
    
    private static boolean isISetter(IMethod method) {
        // eliminacja na podstawie deklaracji
        if(IClass.OBJECT.equals(method.parent())) {
            return false;
        }
        List<IClass<?>> args = method.arguments().types().list();
        if(args.size()!=2) {
            return false;
        }
        if(!args.get(0).equals(INT_ARG)) {
            return false;
        }
        if(!method.returns().attributes().contains(IAttribute.VOID) && !method.returns().equals(method.parent())) {
            return false;
        }

        String name = method.name();
        // akceptacja klasycznego settera
        if(name.startsWith("set") && name.length()>3 && Character.isUpperCase(name.charAt(3)) ) {
            return true;
        }
        return false;
    }
        
    private static String asPropertyName(String name) {
        if(name.startsWith("is") ) {
            return StringUtils.uncapitalize(name.substring(2));
        }
        if( name.startsWith("get") ) {
            return StringUtils.uncapitalize(name.substring(3));
        }
        if( name.startsWith("set") ) {
            return StringUtils.uncapitalize(name.substring(3));
        }
        throw new IllegalArgumentException("Unknown property pattern: " + name);
    }

    
}
