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

import lombok.RequiredArgsConstructor;

import net.ranides.assira.collection.lists.RTList;
import net.ranides.assira.collection.maps.OpenMap;
import net.ranides.assira.reflection.*;

import java.io.Serializable;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.Map;

/**
 * IContext implementation which stores generic params used by "derived" class
 * Generic parameters are resolved using reflection.
 *
 * Please note that reflective context is hierarchical: it takes information from enclosing
 * method, inherited classes, outer classes etc
 *
 * @author Ranides Atterwim {@literal <ranides@gmail.com>}
 */
public final class RContext extends AContext implements Serializable {

    private static final FCache<IContext> CACHE = new FCache<>();

    private final AContext parent;

    private final Type derived;

    private final RTList<TypeVariable> keys = new RTList<>();
    
    private final RTList<Type> vars = new RTList<>();
    
    private RContext(IContext parent, Type type) {
        this.parent = (AContext)parent;
        this.derived = type;
        add(type);
    }
    
    static IContext create(IContext parent, Type type) {
        return CACHE.get(parent, type, () -> new RContext(parent, type));
    }
    
    private void add(Type type) {
        if(type instanceof Class<?>) {
            addc((Class<?>)type);
        }
        if(type instanceof ParameterizedType) {
            addp((ParameterizedType)type);
        }
    }

    private void addc(Class<?> ctype) {
        Class<?> sc = ctype.getSuperclass();

        if(sc == null || sc.equals(Object.class)) {
            return;
        }

        if(sc.getTypeParameters().length != 0) {
            add(ctype.getGenericSuperclass());
        }

        add(sc);
        try {
            add(ctype.getEnclosingClass());
        } catch (IncompatibleClassChangeError e) {
            // ca be thrown by dynamic classes: it is not our fault :-)
        }
    }

    private void addp(ParameterizedType type) {
        keys.addAll(raw(type).getTypeParameters());
        vars.addAll(type.getActualTypeArguments());

        add(type.getOwnerType());
    }

    private static Class<?> raw(ParameterizedType type) {
        return (Class<?>)type.getRawType();
    }

    @Override
    public boolean derived(Type type) {
        return derived.equals(type) || parent.derived(type);
    }

    @Override
    protected Type resolve(String type) {
        for(int i=0, n=keys.size(); i<n; i++) {
            if( keys.get(i).getName().equals(type) ) {
                return resolve0(vars.get(i));
            }
        }
        return parent.resolve(type);
    }

    @Override
    protected Type resolve(TypeVariable type) {
        for(int i=0, n=keys.size(); i<n; i++) {
            if( match(keys.get(i), type) ) {
                return resolve0(vars.get(i));
            }
        }
        return parent.resolve(type);
    }
    
    private boolean match(TypeVariable key, TypeVariable type) {
        return key.getName().equals(type.getName());
    }
    
    private Type resolve0(Type type) {
        if(type instanceof TypeVariable) {
            return parent.resolve((TypeVariable)type);
        } else {
            return type;
        }
    }

    @Override
    public Map<String, Type> map() {
        OpenMap<String, Type> map = new OpenMap<>();
        for(int i=0, n=keys.size(); i<n; i++) {
            map.put(keys.get(i).getName(), vars.get(i));
        }
        map.putAll(super.map());
        return map;
    }

    protected Object writeReplace() {
        String[] cKeys = keys.stream().map(TypeVariable::getName).toArray(String[]::new);
        IClass<?>[] cVars = vars.stream().map(this::typeinfo).toArray(IClass[]::new);
        return new SContext(parent, new RTList<>(cKeys), new RTList<>(cVars));
    }

    @RequiredArgsConstructor
    private static final class SContext extends AContext implements Serializable {

        private final AContext parent;
        private final RTList<String> keys;
        private final RTList<IClass<?>> vars;

        protected Type resolve(String type) {
            for(int i=0, n=keys.size(); i<n; i++) {
                if( keys.get(i).equals(type) ) {
                    return vars.get(i).reflective();
                }
            }
            return parent.resolve(type);
        }

        @Override
        protected Type resolve(TypeVariable type) {
            for(int i=0, n=keys.size(); i<n; i++) {
                if( match(keys.get(i), type) ) {
                    return vars.get(i).reflective();
                }
            }
            return parent.resolve(type);
        }

        private boolean match(String key, TypeVariable type) {
            return key.equals(type.getName());
        }

        @Override
        public Map<String, Type> map() {
            OpenMap<String, Type> map = new OpenMap<>();
            for(int i=0, n=keys.size(); i<n; i++) {
                map.put(keys.get(i), vars.get(i).reflective());
            }
            map.putAll(super.map());
            return map;
        }

    }
}
