/*
 * @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.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import net.ranides.assira.collection.ArrayUtils;
import net.ranides.assira.generic.Serializer;
import net.ranides.assira.generic.ValueUtils;
import net.ranides.assira.generic.Wrapper;
import net.ranides.assira.reflection.GenericClass;
import net.ranides.assira.reflection.TypeFactory;
import net.ranides.assira.text.LexicalCast;
import net.ranides.assira.text.Strings;

/**
 * Compiled expression usable for read/write value from objects. 
 * You can access recursively properties from properties. 
 * 
 * <h3>Supported syntax:</h3>
 * <table>
 *  <tr><td>.name</td><td>read field or bean-property from object</td></tr>
 *  <tr><td>name</td><td>read field or bean-property from object</td></tr>
 *  <tr><td>#name</td><td>get key from a {@link Map}</td></tr>
 *  <tr><td>[number]</td><td>get item from an array-like object</td></tr>
 *  <tr><td>number</td><td>get item from an array-like object</td></tr>
 * </table>
 * 
 * <h3>Supported array-like objects:</h3>
 * <ul>
 *  <li>{@link List}</li>
 *  <li>array</li>
 *  <li>{@link Matcher}</li>
 * </li>
 * 
 * <h3>Example:</h3>
 * <pre>
 *      object.property.list[4].users#id.name
 * </pre>
 *
 */
public final class Resolver implements Serializable {
    
    private static final long serialVersionUID = 1L;
    
    private static final Pattern RE_RESOLVE = Pattern.compile(
        "(?:\\G\\.?([^\\.\\[\\]#]+))|(?:\\G#([^\\.\\[\\]#]+))|(?:\\G\\[([0-9]+)\\])"
    );
    
    private final String expression;
    private final Token[] tokens;
    
    private Resolver(String expression) {
        this.expression = expression;

        ArrayList<Token> list = new ArrayList<>();
        Matcher hit = RE_RESOLVE.matcher(expression);
        while(hit.find()) {
            list.add(compileToken(hit));
            if(expression.length() == hit.end()) {
                tokens = list.toArray(new Token[list.size()]);
                return;
            }
        }
        throw new IllegalArgumentException("Invalid expression");
    }
    
    public static Resolver compile(String expression) {
        return new Resolver(expression);
    }
    
    public static Object get(Object object, String expression) {
        return Resolver.compile(expression).get(object);
    }
    
    public static void set(Object object, String expression, Object value) {
        Resolver.compile(expression).set(object, value);
    }
    
    public static Wrapper<Object> reference(Object object, String expression) {
        return Resolver.compile(expression).reference(object);
    }
    
    public Object get(Object object) {
        Object context = object;
        for(Token token : tokens) {
            context = token.get(context);
        }
        return context;
    }
    
    public void set(Object object, Object value) {
        Object context = object;
        int n = tokens.length-1;
        for(int i=0; i<n; i++) {
            context = tokens[i].get(context);
        }
        tokens[n].set(context, value);
    }
   
    public Wrapper<Object> reference(Object object) {
        TypeContext context = new TypeContext().bind(object);
        int n = tokens.length-1;
        for(int i=0; i<n; i++) {
            context = tokens[i].typeof(context);
        }
        GenericClass type = tokens[n].typeof(context).type;
        return new RWrapper(context.value, type, tokens[n]);
    }
    
    private static class RWrapper extends Wrapper<Object> {
        
        private final Object context;
        private final GenericClass type;
        private final Token token;

        RWrapper(Object context, GenericClass type, Token token) {
            this.context = context;
            this.type = type;
            this.token = token;
        }

        @Override
        public GenericClass type() {
            return type;
        }
        
        @Override
        public Object get() {
            return token.get(context);
        }
        
        @Override
        public void set(Object value) {
            token.set(context, value);
        }
        
        @Override
        public void put(Object value) {
            token.set(context, LexicalCast.cast(value, type));
        }

    }
    
    public String expression() {
        return expression;
    }
    
    private static Token compileToken(Matcher hit) throws NumberFormatException {
        if( isint(hit.group(1)) ) {
            return new AToken(hit.group(1));
        }
        if( null != hit.group(1) ) {
            return new BToken(hit.group(1));
        }
        if( null != hit.group(2) ) {
            return new MToken(hit.group(2));
        }
        if( null != hit.group(3) ) {
            return new AToken(hit.group(3));
        }
        throw new IllegalArgumentException("Invalid expression fragment:" + hit.group());
    }
    
    private static boolean isint(String value) {
        if(null == value) {
            return false;
        }
        for(int i=0, n=value.length(); i<n; i++) {
            if( !Character.isDigit(value.charAt(i)) ) {
                return false;
            }
        }
        return true;
    }
    
    @Override
    public int hashCode() {
        return expression.hashCode();
    }

    @Override
    public boolean equals(Object object) {
        if(!(object instanceof Resolver)) {
            return false;
        }
        Resolver other = (Resolver)object;
        return Strings.isEqual(this.expression, other.expression);
    }
    
    private Object writeReplace() {
        return Serializer.proxy(this, expression);
    }
    
    
    private interface Token {
        
        Object get(Object context);
        
        void set(Object context, Object value);
        
        TypeContext typeof(TypeContext context);
        
    }
    
    private static class AToken implements Token {
        
        private final int index;

        public AToken(String index) {
            this.index = Integer.parseInt(index);
        }

        @Override
        public Object get(Object context) {
            if(context instanceof Matcher) {
                return ValueUtils.or(((Matcher)context).group(index), "");
            }
            if(context instanceof CharSequence) {
                return ((CharSequence)context).charAt(index);
            }
            return ArrayUtils.wrap(context).get(index);
        }

        @Override
        public void set(Object context, Object value) {
            if(context instanceof Matcher) {
                throw new UnsupportedOperationException("Can't modify object: Matcher");
            }
            if(context instanceof CharSequence) {
                throw new UnsupportedOperationException("Can't modify object: CharSequence");
            }
            ArrayUtils.wrap(context).set(index, value);
        }

        @Override
        public TypeContext typeof(TypeContext context) {
            if(context.is(Matcher.class)) {
                TypeContext ret = new TypeContext(String.class);
                if(null != context.value) {
                    ret.bind( ValueUtils.or(((Matcher)context.value).group(index), "") );
                }
                return ret;
            }
            if(context.is(CharSequence.class)) {
                TypeContext ret = new TypeContext(char.class);
                if(null != context.value) {
                    ret.bind(((CharSequence)context.value).charAt(index));
                }
                return ret;
            }
            if(context.type.rawType().isArray()) {
                TypeContext ret = new TypeContext(context.type.rawType().getComponentType());
                if(null != context.value) {
                    ret.bind( ArrayUtils.wrap(context.value).get(index) );
                }
                return ret;
            }
            if(context.value instanceof List) {
                TypeContext ret = new TypeContext(context.type.params()[0]);
                if(null != context.value) {
                    ret.bind( ArrayUtils.wrap(context.value).get(index) );
                }
                return ret;
            }
            throw new IllegalArgumentException("Invalid argument, type is not an array: " + context);
        }
        
    }
    
    private static class BToken implements Token {
        
        private final String index;

        public BToken(String index) {
            this.index = index;
        }

        @Override
        public Object get(Object context) {
            return BeanInspector.forObject(context).get(context, index);
        }

        @Override
        public void set(Object context, Object value) {
            BeanInspector.forObject(context).put(context, index, value);
        }

        @Override
        public TypeContext typeof(TypeContext context) {
            if( null == context.value ) {
                BeanInspector model = BeanInspector.forClass(context.type.rawType());
                return new TypeContext(model.typeof(index));
            } else {
                BeanInspector model = BeanInspector.forObject(context.value);
                return new TypeContext(model.typeof(index)).bind(model.get(context.value, index));
            }
        }
                
    }
    
    private static class MToken implements Token {

        private final String key;

        public MToken(String key) {
            this.key = key;
        }

        @Override
        public Object get(Object context) {
            return ((Map<?,?>)context).get(key);
        }

        @Override
        public void set(Object context, Object value) {
            ((Map<String,Object>)context).put(key, value);
        }

        @Override
        public TypeContext typeof(TypeContext context) {
            if( null == context.value) {
                return new TypeContext( context.type.params()[1]);
            } else {
                return new TypeContext( context.type.params()[1])
                    .bind(((Map<?,?>)context.value).get(key));
            }
        }
        
    }
    
    private static class TypeContext {
        
        private GenericClass type;
        private Object value;
        
        public TypeContext() {
            // do nothing
        }
        
        public TypeContext(Class<?> type) {
            this(TypeFactory.construct(type));
        }

        public TypeContext(GenericClass type) {
            this.type = type;
            this.value = null;
        }
        
        public boolean is(Class<?> type) {
            if(value != null && type.isInstance(value)) {
                return true;
            }
            return type.isAssignableFrom(this.type.rawType());
        }
        
        public TypeContext bind(Object value) {
            this.value = value;
            if( null == value ) {
                return this;
            }
            if((null == type) || (!type.isParametrised() && !type.rawType().isPrimitive()) ) {
                this.type = TypeFactory.construct(value.getClass());
            }
            return this;
        }

        @Override
        public String toString() {
            return "TypeContext{" + "type=" + type + ", value=" + value + '}';
        }
        
    }
}
