/*
 * @author Ranides Atterwim {@literal <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 net.ranides.assira.generic.CompareUtils;
import net.ranides.assira.reflection.*;
import net.ranides.assira.reflection.util.ClassUtils;

/**
 * This internal intermediate object contains all data about detected bean property.
 * For example type and accessor methods.
 *
 * It is created by RBeanModel and used by FBeanModel to create instance of specific BeanProperty implementation.
 *
 * Please note:
 * We try to not throw exception on unexpected ill-formed methods which look like accessor, but they aren't.
 * We simply ignore such methods. If you want to understand why some property is invisible in BeamModel, you should use
 * #verify method.
 *
 * It is obvious and reasonable: we shouldn't throw errors for proper classes over which we do not have control just
 * because they do not match some conventions (especially because some of them have really awkward edge cases)
 *
 * @author Ranides Atterwim {@literal <ranides@gmail.com>}
 */
public class RBeanPropertyBuilder {
    
    private final IClass<?> parent;
    private final String name;

    private final List<IMethod> vsetters = new ArrayList<>(2);
    private final List<IMethod> isetters = new ArrayList<>(2);

    private IMethod vgetter;
    private IMethod vsetter;
    
    private IMethod igetter;
    private IMethod isetter;
    
    private IClass<?> type;
    private IClass<?> item;

    RBeanPropertyBuilder(IClass<?> parent, String name) {
        this.parent = parent;
        this.name = name;
    }

    /**
     * Name of property
     *
     * @return String
     */
    public String name() {
        return name;
    }

    /**
     * Ordinary getter, returning full content of property.
     *
     * @return IMethod
     */
    public IMethod vgetter() {
        return vgetter;
    }

    /**
     * Ordinary setter, changing full content of property.
     *
     * @return IMethod
     */
    public IMethod vsetter() {
        return vsetter;
    }

    /**
     * Indexed getter, returning element stored inside property under specified int index.
     *
     * @return IMethod
     */
    public IMethod igetter() {
        return igetter;
    }

    /**
     * Indexed setter, changing element stored inside property under specified int index.
     *
     * @return IMethod
     */
    public IMethod isetter() {
        return isetter;
    }

    /**
     * Type of property
     *
     * @return IClass
     */
    public IClass<?> type() {
        return type;
    }

    /**
     * If property is indexed or array, returns type of component
     *
     * @return IClass
     */
    public IClass<?> item() {
        return item;
    }
    
    void vgetter(IMethod m) {
        vgetter = m;
    }
    
    void vsetter(IMethod m) {
        vsetters.add(m);
    }
    
    void igetter(IMethod m) {
        igetter = m;
    }
    
    void isetter(IMethod m) {
        isetters.add(m);
    }
    
    boolean validate(Collection<BeanException> errors) {
        initVSetter(errors);
        initItem(errors);
        initISetter(errors);
        initType(errors);
        return type != null;
    }

    private void initVSetter(Collection<BeanException> errors) throws IllegalArgumentException {
        if(vgetter != null) {
            type = vgetter.returns();
            vsetter = vsetters.stream().filter(m -> m.matches(type)).findFirst().orElse(null);

            if(!vgetter.parent().attributes().has(IAttribute.PUBLIC)) {
                vgetter.accessible();
            }
        }
        if(vsetter == null) {
            if( vsetters.size() > 1) {
                if(type != null) {
                    vsetter = vsetters.stream().filter(m -> m.matches(type)).findFirst().orElse(null);
                }
                if(errors != null) {
                    errors.add(new BeanException(parent, "WARN: Ambiguous setter: " + name));
                }
            }
            else if( vsetters.size() == 1) {
                vsetter = vsetters.get(0);
                type = vsetter.arguments().types().first().get();
            }
		}
        if(vsetter != null) {
            if(!vsetter.parent().attributes().has(IAttribute.PUBLIC)) {
                vsetter.accessible();
            }
        }
    }
    
    private void initItem(Collection<BeanException> errors) throws IllegalArgumentException {
        if(type !=null && type.attributes().has(IAttribute.ARRAY)) {
            item = type.component();
        }
        if(igetter != null) {
            IClass<?> iitem = igetter.returns();
            if( item!=null && !iitem.equals(item) ) {
                if(errors != null) {
                    errors.add(new BeanException(parent, "WARN: Invalid array getter for: " + name));
                }
                item = null;
            } else {
                item = iitem;
            }
        }
    }
    
    private void initISetter(Collection<BeanException> errors) throws IllegalArgumentException {
        if(item != null) {
            isetter = isetters.stream().filter(m -> m.matches(IClass.INT, item)).findFirst().orElse(null);
        } else {
            if( isetters.size() > 1) {
                if(errors != null) {
                    errors.add(new BeanException(parent, "WARN: Ambiguous property indexed setters for: " + name));
                }
                isetter = null;
                item = null;
            }
            if( isetters.size() == 1) {
                isetter = isetters.get(0);
                item = isetter.arguments().types().list().get(1);
            }
        }
    }
    
    private void initType(Collection<BeanException> errors) throws IllegalArgumentException {
        if( null != item && null!=type) {
            IClass<?> ctype = null;
            if(type.attributes().has(IAttribute.ARRAY)) {
                ctype = type.component();
            }
            if(type.isSubclass(IClass.generic(List.class, item))) {
                ctype = type.method("get").get().returns();
            }
            if (ctype == null) {
                // if ctype is not array or collection, then it is false positive:
                // property is not indexed at all and we ignore it safely
                item = null;
                if(errors != null) {
                    errors.add(new BeanException(parent, "HINT: False indexed property: " + name));
                }
            }
            if (!CompareUtils.equals(item, ctype)) {
                item = null;
                if(errors != null) {
                    errors.add(new BeanException(parent, "WARN: Invalid type of indexed property: " + name));
                }
            }
        }
        if(null == type && null==item) {
            if(errors != null) {
                errors.add(new BeanException(parent, "WARN: Unknown type of indexed property: " + name));
            }
        }
        if(type == null && item != null) {
            type = IClass.generic(List.class, ClassUtils.box(item));
        }
        
    }

}
