/*
 * @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 lombok.RequiredArgsConstructor;
import net.ranides.assira.collection.iterators.IteratorUtils;
import net.ranides.assira.collection.maps.*;
import net.ranides.assira.collection.query.CQuery;
import net.ranides.assira.collection.sets.FlatSet;
import net.ranides.assira.collection.sets.OpenSet;
import net.ranides.assira.generic.Ref;
import net.ranides.assira.generic.SerializationUtils;
import net.ranides.assira.generic.Wrappers.AbstractWrapper;
import net.ranides.assira.reflection.*;
import net.ranides.assira.reflection.util.MemberUtils;

import java.io.Serializable;
import java.util.*;
import java.util.Map.Entry;

/**
 * This is basic BeanModel implementation.
 * It uses reflection to inspect provided type and stores collected data about
 * detected properties and methods.
 *
 * @author Ranides Atterwim {@literal <ranides@gmail.com>}
 */
public class RBeanModel implements BeanModel, Serializable {
    
    private static final Set<String> OBJECT_METHODS = new OpenSet<>(new String[]{"toString","hashCode","equals"});
    
    private static final IArguments INT_ARGS = IArguments.typeinfo(IClass.INT);

    private final IClass<?> type;
    private final Map<String, BeanMethod> methods;
    private final Map<String, BeanMethod> fluentGet;
    private final Map<String, BeanMethod> fluentSet;
    private final Map<String, BeanProperty> properties;
    private final Set<String> keys;

    RBeanModel(IClass<?> type) {
        this(type, null);
    }

    RBeanModel(IClass<?> type, Collection<BeanException> errors) {
        this.type = type;
        
        Map<String, RBeanPropertyBuilder> pinfo = new LazyMap<>(new OpenMap<>(), name -> new RBeanPropertyBuilder(type, name));
        MultiMap<String, IMethod> minfo = new OpenMultiMap<>();

        fluentGet = new OpenMap<>();
        fluentSet = new OpenMap<>();

        if(errors!=null && !type.isPublic()) {
            errors.add(new BeanException(type, "Unsafe reflective non-public scan: " + type));
        }

        for (IMethod m : type.methods().stream()) {
            if (m.parent().equals(IClass.OBJECT) && !OBJECT_METHODS.contains(m.name())) {
                continue;
            }

            if (isVGetter(m)) {
                pinfo.get(MemberUtils.asPropertyName(m.name())).vgetter(m);
            } else if (isIGetter(m)) {
                pinfo.get(MemberUtils.asPropertyName(m.name())).igetter(m);
            } else if (isVSetter(m)) {
                pinfo.get(MemberUtils.asPropertyName(m.name())).vsetter(m);
            } else if (isISetter(m)) {
                pinfo.get(MemberUtils.asPropertyName(m.name())).isetter(m);
            } else {
                minfo.put(m.name(), m);
                if (m.arguments().count() == 0 && !m.parent().equals(IClass.OBJECT)) {
                    fluentGet.put(m.name(), new RBeanMethod(errors, m));
                }

                if (m.arguments().count() == 1 && !m.parent().equals(IClass.OBJECT) && (m.returns().isVoid() || m.returns().isSuper(m.parent()))) {
                    fluentSet.put(m.name(), new RBeanMethod(errors, m));
                }
            }
        }

        methods = new OpenMap<>();
        for(String m : minfo.uniqueKeySet()) {
            methods.put(m, FBeanModel.resolve(errors, minfo.getAll(m)));
        }
        properties = new OpenMap<>();
        for(Entry<String, RBeanPropertyBuilder> item : pinfo.entrySet()) {
            BeanProperty resolved = FBeanModel.resolve(errors, item.getValue());
            if(resolved != null) {
                properties.put(item.getKey(), resolved);
                fluentGet.remove(item.getKey());
            }
        }
        this.keys = new FlatSet<String>()
            .join(properties.keySet())
            .join(fluentGet.keySet())
            .join(fluentSet.keySet());
    }

    @Override
    public IClass<?> type() {
        return type;
    }

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

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

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

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

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

    @Override
    public Set<String> fluent() {
        return keys;
    }

    @Override
    public FluentMap fluent(Object that) {
        return new RFluentMap(that);
    }

    @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;
    }

    protected Object writeReplace() {
        return SerializationUtils.proxy(this, type);
    }
    
    private static boolean isVGetter(IMethod method) {
        // eliminacja na podstawie deklaracji
        if(IClass.OBJECT.equals(method.parent())) {
            return false;
        }
        if(!method.arguments().isEmpty()) {
            return false;
        }
        IAttributes 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;
        }
        if(method.returns().isVoid()) {
            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)) && method.returns().isBoolean()) {
            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(IClass.INT)) {
            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 class RFluentMap extends AMap<String, Object> implements FluentMap {

        private final Object that;

        private final Set<Entry<String, Object>> entries;

        public RFluentMap(Object that) {
            this.that = that;
            this.entries = new FluentEntries(that);
        }

        @Override
        public Object unwrap() {
            return that;
        }

        @Override
        public BeanModel model() {
            return RBeanModel.this;
        }

        @Override
        public Set<String> keySet() {
            return keys;
        }

        @Override
        public Set<Entry<String, Object>> entrySet() {
            return entries;
        }

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

        @Override
        public boolean containsKey(Object key) {
            return properties.containsKey(key) || fluentGet.containsKey(key) || fluentSet.containsKey(key);
        }

        @Override
        public boolean containsValue(Object value) {
            return properties.values().stream().map(p -> p.get(that)).anyMatch(v -> Objects.equals(v, value)) ||
                 fluentGet.values().stream().map(p -> p.invoke(that)).anyMatch(v -> Objects.equals(v, value));
        }

        @Override
        public Object put(String key, Object value) {
            BeanProperty p = properties.get(key);
            if(p != null) {
                return p.replace(that, value);
            }
            BeanMethod s = fluentSet.get(key);
            if(s != null) {
                s.invoke(that, value);
                return null;
            }
            BeanMethod m = fluentGet.get(key);
            if(m != null) {
                throw FBeanModel.newUnsupportedOperationException("Computed fluent property " + key + " is read-only");
            }
            throw FBeanModel.newUnsupportedOperationException("No property with name " + key);
        }

        @Override
        public Object get(Object key) {
            if(!(key instanceof String)) {
                return null;
            }
            BeanProperty p = properties.get(key);
            if(p != null) {
                return p.get(that);
            }
            BeanMethod m = fluentGet.get(key);
            if(m != null) {
                return m.invoke(that);
            }
            BeanMethod s = fluentSet.get(key);
            if(s != null) {
                throw FBeanModel.newUnsupportedOperationException("Computed fluent property " + key + " is write-only");
            }
            return null;
        }

        @Override
        public IClass<?> getType(String key) {
            BeanProperty p = properties.get(key);
            if(p != null) {
                return p.type();
            }
            BeanMethod m = fluentGet.get(key);
            if(m != null) {
                return m.returns();
            }
            BeanMethod s = fluentSet.get(key);
            if(s != null) {
                return s.arguments().types().first().get();
            }

            return null;
        }

        public IClass<?> getParent(String key) {
            BeanProperty p = properties.get(key);
            if(p != null) {
                return p.parent();
            }
            BeanMethod m = fluentGet.get(key);
            if(m != null) {
                return m.parent();
            }
            BeanMethod s = fluentSet.get(key);
            if(s != null) {
                return s.parent();
            }

            return null;
        }

        @Override
        public BeanPropertyBinding<Object> getBinding(String key) {
            if(properties.containsKey(key)) {
                return properties.get(key).bind(that);
            }
            if(fluentGet.containsKey(key) || fluentSet.containsKey(key)) {
                return new RFluentBinding(key);
            }
            return null;
        }

        @RequiredArgsConstructor
        private class RFluentBinding implements BeanPropertyBinding<Object>, AbstractWrapper<Object> {

            private final String name;

            @Override
            public IClass<?> type() {
                return RFluentMap.this.getType(name);
            }

            @Override
            public Object get() {
                return RFluentMap.this.get(name);
            }

            @Override
            public void set(Object value) {
                RFluentMap.this.put(name, value);
            }

            @Override
            public Object object() {
                return RFluentMap.this.that;
            }

            @Override
            public String name() {
                return name;
            }

            @Override
            public boolean isReadable() {
                return fluentGet.containsKey(name);
            }

            @Override
            public boolean isWritable() {
                return fluentSet.containsKey(name);
            }

            @Override
            public IClass<?> parent() {
                return RFluentMap.this.getParent(name);
            }
        }
    }

    @RequiredArgsConstructor
    private class FluentEntries extends AbstractSet<Entry<String, Object>> {

        private final Object that;

        @Override
        public Iterator<Entry<String, Object>> iterator() {
            return IteratorUtils.concat(
                    IteratorUtils.map(properties.values().iterator(), p -> new PEntry(p, that)),
                    IteratorUtils.map(fluentGet.values().iterator(), p -> new MGEntry(p, that)),
                    IteratorUtils.map(fluentSet.values().iterator(), p -> new MSEntry(p, that))
            );
        }

        @Override
        public int size() {
            return properties.size() + fluentGet.size() + fluentSet.size();
        }

        @Override
        public boolean contains(Object o) {
            if(!(o instanceof Entry<?,?>)) {
                return false;
            }

            Entry<?,?> entry = (Entry<?, ?>) o;
            Object key = entry.getKey();

            if(!(key instanceof String)) {
                return false;
            }
            BeanProperty p = properties.get(key);
            if(p != null) {
                return Objects.equals(entry.getValue(), p.get(that));
            }
            BeanMethod m = fluentGet.get(key);
            if(m != null) {
                return Objects.equals(entry.getValue(), m.invoke(that));
            }
            // strictly speaking, we can't validate value, so we should return "undefined"
            if(fluentSet.containsKey(key)) {
                return true;
            }
            return false;
        }
    }

    @RequiredArgsConstructor
    private static class PEntry implements Entry<String, Object> {

        private final BeanProperty p;

        private final Object that;

        @Override
        public String getKey() {
            return p.name();
        }

        @Override
        public Object getValue() {
            return p.get(that);
        }

        @Override
        public Object setValue(Object value) {
            return p.replace(that, value);
        }
    }

    @RequiredArgsConstructor
    private static class MGEntry implements Entry<String, Object> {

        private final BeanMethod p;

        private final Object that;

        @Override
        public String getKey() {
            return p.name();
        }

        @Override
        public Object getValue() {
            return p.invoke(that);
        }

        @Override
        public Object setValue(Object value) {
            throw FBeanModel.newUnsupportedOperationException("Computed fluent property " + p.name() + " is read-only");
        }
    }

    @RequiredArgsConstructor
    private static class MSEntry implements Entry<String, Object> {

        private final BeanMethod s;

        private final Object that;

        @Override
        public String getKey() {
            return s.name();
        }

        @Override
        public Object getValue() {
            throw FBeanModel.newUnsupportedOperationException("Computed fluent property " + s.name() + " is write-only");
        }

        @Override
        public Object setValue(Object value) {
            return s.invoke(that, value);
        }
    }

    
}
