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

package net.ranides.assira.reflection;

import java.io.Serializable;
import java.util.*;
import java.util.regex.Pattern;

import net.ranides.assira.generic.SerializationUtils;
import net.ranides.assira.generic.Wrapper;
import net.ranides.assira.generic.Wrappers;
import net.ranides.assira.generic.Wrappers.AbstractWrapper;
import net.ranides.assira.text.StringUtils;

/**
 * ResolvePattern represents compiled string expression used to read properties from object.
 *
 * You can construct expressions which read nested values, for example:
 *      "transfer.sender.account.name"
 * You can use dots to separate fields, or you can use brackets:
 *      "transfer[sender][account][name]"
 *      "transfer[sender].account[name]"
 * You can read elements from array/list/map using both ways:
 *      "file.lines.7.text"
 *      "file.lines[7].text
 *
 * Please note that default configration contains some special behaviour.
 * Most noticeable models are:
 *  - {@link ResolveModel#CONTEXT}
 *  - {@link ResolveModel#MATCHER}
 *  - {@link ResolveModel#BEAN} (default handler for unrecognized objects)
 *
 * @author Ranides Atterwim {@literal <ranides@gmail.com>}
 */
public class ResolvePattern implements Serializable {
    
    private static final long serialVersionUID = 1L;
    
    private static final Pattern RE_INDEX = Pattern.compile("\\[([^\\]]+)\\]");

    private final ResolveConfiguration configuration;
    
    private final ResolveStrategy handler;
    
    private final String expression;
    
    private final String[] tokens;
    
    private ResolvePattern(ResolveConfiguration configuration, ResolveStrategy handler, String expression) {
        this.configuration = configuration;
        this.handler = handler;
        String[] array = StringUtils.replace(expression, RE_INDEX, m -> "."+m.group(1)+".").split("\\.");
        this.expression = expression;
        this.tokens = Arrays.stream(array).map(String::trim).filter(p->!p.isEmpty()).toArray(String[]::new);
    }

    @Override
    public int hashCode() {
        return Arrays.hashCode(tokens);
    }

    @Override
    public boolean equals(Object object) {
        if (this == object) {
            return true;
        }
        if (object == null || getClass() != object.getClass()) {
            return false;
        }
        ResolvePattern that = (ResolvePattern) object;
        return Arrays.equals(tokens, that.tokens);
    }

    @Override
    public String toString() {
        return StringUtils.join(tokens, ".");
    }

    /**
     * Factory: compiles provided expression using default ResolveConfiguration.
     *
     * Compiled expression uses {@link ResolveStrategy#THROW} and it will throw ResolveException on error.
     *
     * @param expression expression
     * @return ResolvePattern
     */
    public static ResolvePattern compile(String expression) {
        return new ResolvePattern(ResolveConfiguration.DEFAULT, ResolveStrategy.THROW, expression);
    }

    /**
     * Factory: compiles provided expression using default ResolveConfiguration and provided ResolveStrategy.
     *
     * ResolveStrategy defines error handler of compiled expression. For example,
     * it will throw if you passed {@link ResolveStrategy#THROW},
     * but it will return string with quoted expression if you passed {@link ResolveStrategy#FORMAT}
     *
     * @param handler handler
     * @param expression expression
     * @return ResolvePattern
     */
    public static ResolvePattern compile(ResolveStrategy handler, String expression) {
        return new ResolvePattern(ResolveConfiguration.DEFAULT, handler, expression);
    }

    /**
     * Factory: compiles provided expression using provided ResolveConfiguration and ResolveStrategy.
     *
     * ResolveConfiguration defines rules for handling different objects in special way.
     *
     * ResolveStrategy defines error handler of compiled expression. For example,
     * it will throw if you passed {@link ResolveStrategy#THROW},
     * but it will return string with quoted expression if you passed {@link ResolveStrategy#FORMAT}
     *
     * @param configuration configuration
     * @param handler handler
     * @param expression expression
     * @return ResolvePattern
     */
    public static ResolvePattern compile(ResolveConfiguration configuration, ResolveStrategy handler, String expression) {
        return new ResolvePattern(configuration, handler, expression);
    }

    /**
     * Returns expression string used to create this ResolvePattern
     *
     * @return String
     */
    public String expression() {
        return expression;
    }

    /**
     * Reads value of property from "that" object.
     *
     * Behaviour for error handling is defined by {@link ResolveStrategy}.
     *
     * @param that that
     * @return Object
     */
    public Object get(Object that) {
        ResolveModel.ResolveStatus status = new ResolveModel.ResolveStatus();
        for(int i=0,n=tokens.length-1; i<n; i++) {
            that = configuration.get(status, that, tokens[i]);
            if(status.error) {
                return handler.get(this, tokens[i]);
            }
        }
        Object out = configuration.get(status, that, tokens[tokens.length-1]);
        if(status.error) {
            return handler.get(this);
        }
        return out;
    }

    /**
     * Changes value of property from "that" object.
     *
     * Behaviour for error handling is defined by {@link ResolveStrategy}.
     *
     * @param that that
     * @param value value
     */
    public void set(Object that, Object value) {
        ResolveModel.ResolveStatus status = new ResolveModel.ResolveStatus();
        for(int i=0,n=tokens.length-1; i<n; i++) {
            that = configuration.get(status, that, tokens[i]);
            if(status.error) {
                handler.get(this, tokens[i]);
            }
        }
        configuration.set(status, that, tokens[tokens.length-1], value);
        if(status.error) {
            handler.set(this);
        }
    }

    /**
     * Changes value of property from "that" object.
     * Returns previous value of property.
     *
     * Behaviour for error handling is defined by {@link ResolveStrategy}.
     *
     * @param that that
     * @param value value
     * @return Object
     */
    public Object replace(Object that, Object value) {
        ResolveModel.ResolveStatus status = new ResolveModel.ResolveStatus();
        for(int i=0,n=tokens.length-1; i<n; i++) {
            that = configuration.get(status, that, tokens[i]);
            if(status.error) {
                handler.get(this, tokens[i]);
            }
        }
        Object out = configuration.replace(status, that, tokens[tokens.length-1], value);
        if(status.error) {
            return handler.replace(this);
        }
        return out;
    }

    /**
     * Returns deduced type of property from "that" type.
     *
     * Behaviour for error handling is defined by {@link ResolveStrategy}.
     *
     * @param that that
     * @return IClass
     */
    public IClass<?> type(Object that) {
        ResolveModel.ResolveStatus status = new ResolveModel.ResolveStatus();
        IClass<?> type = IClass.typefor(that);
        for(int i=0,n=tokens.length-1; i<n; i++) {
            type = configuration.type(status, type, tokens[i]);
            if(status.error) {
                return handler.type(this, tokens[i]);
            }
        }
        IClass<?> out = configuration.type(status, type, tokens[tokens.length-1]);
        if(status.error) {
            return handler.type(this);
        }
        return out;
    }

    /**
     * Creates wrapper which uses this pattern to access data from provided object.
     *
     * Behaviour of wrapper for error handling is defined by {@link ResolveStrategy}.
     *
     * @param that that
     * @return Wrapper
     */
    public Wrapper<Object> bind(Object that) {
        return new RWrapper(that);
    }

    private Object writeReplace() {
        return SerializationUtils.proxy(this, configuration, handler, expression);
    }

    private class RWrapper implements AbstractWrapper<Object> {
        
        private final Object that;

        public RWrapper(Object that) {
            this.that = that;
        }

        @Override
        public Object get() {
            return ResolvePattern.this.get(that);
        }

        @Override
        public void set(Object value) {
            ResolvePattern.this.set(that, value);
        }

        @Override
        public Object replace(Object value) {
            return ResolvePattern.this.replace(that, value);
        }
        
        @Override
        public IClass<?> type() {
            return ResolvePattern.this.type(that);
        }
    
    }


}
