package net.ranides.assira.reflection;

import net.ranides.assira.collection.arrays.NativeArray;
import net.ranides.assira.generic.Ref;
import net.ranides.assira.generic.SerializableCode;
import net.ranides.assira.reflection.impl.RCompareUtils;
import net.ranides.assira.text.ResolveFormat;
import net.ranides.assira.text.StringTraits;

import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;

/**
 * ResolvePattern can handle object of different types in different way.
 * It delegates work to different, specialized ResolveModel instances.
 *
 * For example, we can write ResolveModel for array inspection, for map inspection,
 * or for your own custom object.
 *
 * It is responsibility of ResolveModel itself to tell if some object can be
 * handled by it. ResolvePattern makes tests against object using all provided
 * ResolveModels from ResolveConfiguration and selects first one which told,
 * that it can process the object.
 */
public abstract class ResolveModel extends SerializableCode {

    private static final long serialVersionUID = 1L;

    private static final String ARRAY_LENGTH = "length";

    /**
     * Returns true, if model is able to deduce type of field declared by "that" type
     *
     * Please remember that type deduction works without instance objects.
     *
     * You must return "true" in exactly the same situations as "matches",
     * because obviously you are the one who knows type of value returned by "get"
     *
     * @param that that
     * @param name name
     * @return boolean
     */
    public abstract boolean matchesType(IClass<?> that, String name);

    /**
     * Returns true, if model is able to read field form "that" object
     *
     * You must return "true" in exactly the same situations as "matchesType",
     * because obviously you are the one who knows type of value returned by "get"
     *
     * @param that that
     * @param name name
     * @return boolean
     */
    public abstract boolean matches(Object that, String name);

    /**
     * Reads value of property from "that" object.
     *
     * If property does not exist, you shouldn't throw exception, but
     * you should change "status.error" flag and return null.
     *
     * @param status (output) status
     * @param that that
     * @param name name
     * @return Object
     */
    public abstract Object get(ResolveStatus status, Object that, String name);

    /**
     * Changes value of property from "that" object.
     *
     * If property does not exist, you shouldn't throw exception, but
     * you should change "status.error" flag and do nothing.
     *
     * @param status (output) status
     * @param that that
     * @param name name
     * @param value value
     */
    public abstract void set(ResolveStatus status, Object that, String name, Object value);

    /**
     * Changes value of property from "that" object.
     * Returns previous value of property.
     *
     * If property does not exist, you shouldn't throw exception, but
     * you should change "status.error" flag and return null.
     *
     * @param status (output) status
     * @param that that
     * @param name name
     * @param value value
     * @return Object
     */
    public abstract Object replace(ResolveStatus status, Object that, String name, Object value);

    /**
     * Returns deduced type of property from "that" type.
     *
     * If property does not exist, you shouldn't throw exception, but
     * you should change "status.error" flag and return null.
     *
     * Please remember that type deduction works without instance objects.
     * For example, for array type, you just return "component" type.
     *
     * @param status status
     * @param that that
     * @param name name
     * @return IClass
     */
    public abstract IClass<?> type(ResolveStatus status, IClass<?> that, String name);

    /**
     * Model for array types
     *
     * It will match if the object is array and field name is
     *  - numerical value
     *  - "length"
     *
     */
    public static final ResolveModel ARRAY = new ResolveModel() {
        @Override
        public boolean matches(Object that, String name) {
            return that != null && that.getClass().isArray() && (StringTraits.isNumber(name) || ARRAY_LENGTH.equals(name));
        }

        @Override
        public boolean matchesType(IClass<?> that, String name) {
            return that.attributes().has(IAttribute.ARRAY) && (StringTraits.isNumber(name) || ARRAY_LENGTH.equals(name));
        }

        @Override
        public Object get(ResolveStatus _s, Object that, String name) {
            if(ARRAY_LENGTH.equals(name)) {
                return NativeArray.wrap(that).size();
            }
            return NativeArray.wrap(that).get(Integer.parseInt(name));
        }

        @Override
        public void set(ResolveStatus _s, Object that, String name, Object value) {
            if(ARRAY_LENGTH.equals(name)) {
                throw new ResolveException("Array.length is immutable");
            }
            NativeArray.wrap(that).set(Integer.parseInt(name), value);
        }

        @Override
        public Object replace(ResolveStatus _s, Object that, String name, Object value) {
            if(ARRAY_LENGTH.equals(name)) {
                throw new ResolveException("Array.length is immutable");
            }
            return NativeArray.wrap(that).replace(Integer.parseInt(name), value);
        }

        @Override
        public IClass<?> type(ResolveStatus _s, IClass<?> that, String name) {
            if(ARRAY_LENGTH.equals(name)) {
                return IClass.INT;
            }
            return that.component();
        }
    };

    /**
     * Model for List types.
     *
     * It will match if the object is List and field name is numerical value.
     *
     * Please note that it won't match if you try to call some method declared
     * by list (for example "size"). Such cases are handled by other models.
     *
     */
    public static final ResolveModel LIST = new ResolveModel() {
        @Override
        public boolean matches(Object that, String name) {
            return that instanceof List<?> && StringTraits.isNumber(name);
        }

        @Override
        public boolean matchesType(IClass<?> that, String name) {
            return that != IClass.NULL && RCompareUtils.isRawSuper(IClass.typeinfo(List.class), that) && StringTraits.isNumber(name);
        }

        @Override
        public Object get(ResolveStatus _s, Object that, String name) {
            return ((List<?>) that).get(Integer.parseInt(name));
        }

        @Override
        @SuppressWarnings("unchecked")
        public void set(ResolveStatus _s, Object that, String name, Object value) {
            ((List<Object>) that).set(Integer.parseInt(name), value);
        }

        @Override
        @SuppressWarnings("unchecked")
        public Object replace(ResolveStatus _s, Object that, String name, Object value) {
            List<Object> list = (List<Object>) that;
            int index = Integer.parseInt(name);
            Object prv = list.get(index);
            list.set(index, value);
            return prv;
        }

        @Override
        public IClass<?> type(ResolveStatus _s, IClass<?> that, String name) {
            return that.params().get(0);
        }
    };

    /**
     * Model for Map types.
     *
     * It will match if the object implements Map
     *
     * Please note that it is impossible to call any method declared by Map.
     * Every field will be access using "get" and "put" operations.
     */
    public static final ResolveModel MAP = new ResolveModel() {
        @Override
        public boolean matches(Object that, String _name) {
            return that instanceof Map<?,?>;
        }

        @Override
        public boolean matchesType(IClass<?> that, String _name) {
            return that!=IClass.NULL && RCompareUtils.isRawSuper(IClass.typeinfo(Map.class), that);
        }

        @Override
        @SuppressWarnings("unchecked")
        public Object get(ResolveStatus status, Object that, String name) {
            if(!((Map<String,Object>)that).containsKey(name)) {
                status.error = true;
            }
            return ((Map<?,?>)that).get(name);
        }

        @Override
        @SuppressWarnings("unchecked")
        public void set(ResolveStatus _s, Object that, String name, Object value) {
            ((Map<String,Object>)that).put(name, value);
        }

        @Override
        @SuppressWarnings("unchecked")
        public Object replace(ResolveStatus _s, Object that, String name, Object value) {
            return ((Map<String,Object>)that).put(name, value);
        }

        @Override
        public IClass<?> type(ResolveStatus _s, IClass<?> that, String name) {
            return that.params().get(1);
        }
    };

    /**
     * Model for Matcher types.
     *
     * It will match if the object is {@link Matcher}
     *
     * Please note that it is impossible to call any method declared by Matcher.
     * Every field will be accessed using "group" method.
     * You can access matched groups by index or by name.
     */
    public static final ResolveModel MATCHER = new ResolveModel() {
        @Override
        public boolean matches(Object that, String _name) {
            return that instanceof Matcher;
        }

        @Override
        public boolean matchesType(IClass<?> that, String _name) {
            return that != IClass.NULL && IClass.typeinfo(Matcher.class).isSuper(that);
        }

        @Override
        public Object get(ResolveStatus _s, Object that, String name) {
            if (StringTraits.isNumber(name)) {
                int index = Integer.parseInt(name);
                if (index >= 0 && index <= ((Matcher) that).groupCount()) {
                    return ((Matcher) that).group(index);
                } else {
                    return ((Matcher) that).group(name);
                }
            } else {
                return ((Matcher) that).group(name);
            }
        }

        @Override
        public void set(ResolveStatus _s, Object that, String name, Object value) {
            throw new ResolveException("Matcher is immutable.");
        }

        @Override
        public Object replace(ResolveStatus _s, Object that, String name, Object value) {
            throw new ResolveException("Matcher is immutable.");
        }

        @Override
        public IClass<?> type(ResolveStatus _s, IClass<?> that, String name) {
            return IClass.typeinfo(String.class);
        }
    };

    /**
     * Model for CharSequence types
     *
     * It will match if the object is CharSequence and field name is numerical value.
     *
     * Please note that it won't match if you try to call some method or getter
     * (for example "length"). Such cases are handled by other models.
     *
     */
    public static final ResolveModel CHARS = new ResolveModel() {
        @Override
        public boolean matches(Object that, String name) {
            return that instanceof CharSequence && StringTraits.isNumber(name);
        }

        @Override
        public boolean matchesType(IClass<?> that, String name) {
            return that != IClass.NULL && IClass.typeinfo(CharSequence.class).isSuper(that) &&  StringTraits.isNumber(name);
        }

        @Override
        public Object get(ResolveStatus _s, Object that, String name) {
            return ((CharSequence) that).charAt(Integer.parseInt(name));
        }

        @Override
        public void set(ResolveStatus _s, Object that, String name, Object value) {
            throw new ResolveException("CharSequence is immutable.");
        }

        @Override
        public Object replace(ResolveStatus _s, Object that, String name, Object value) {
            throw new ResolveException("CharSequence is immutable.");
        }

        @Override
        public IClass<?> type(ResolveStatus _s, IClass<?> that, String name) {
            return IClass.typeinfo(char.class);
        }
    };

    /**
     * Model which works for any object
     *
     * It will match for every non-null value, so it should be inserted into
     * ResolveConfiguration at the very end.
     *
     * It uses {@link BeanModel#fluent(Object)} to call getter method.
     * If there is no getter, then it tries to read field value using reflection.
     *
     * Please note, it read even private fields (it shouldn't)
     *
     * @todo fix resolver: optimize this shit
     */
    public static final ResolveModel BEAN = new ResolveModel() {
        @Override
        public boolean matches(Object that, String _name) {
            return that != null;
        }

        @Override
        public boolean matchesType(IClass<?> that, String _name) {
            return that != IClass.NULL;
        }

        @Override
        public Object get(ResolveStatus status, Object that, String name) {
            BeanModel model = BeanModel.typefor(that);
            Object v = model.fluent(that).get(name);
            if (v != null) {
                return v;
            }
            Iterator<IField> f = field(model, name);
            if (f.hasNext()) {
                return f.next().accessible().get(that);
            }
            status.error = true;
            return null;
        }

        private Iterator<IField> field(BeanModel model, String name) {
            return model.type().fields().require(IAttribute.PUBLIC).require(name).iterator();
        }

        @Override
        public void set(ResolveStatus status, Object that, String name, Object value) {
            BeanModel model = BeanModel.typefor(that);
            if (model.properties().containsKey(name)) {
                model.property(name).get().set(that, value);
                return;
            }
            Iterator<IField> f = field(model, name);
            if (f.hasNext()) {
                f.next().accessible().set(that, value);
                return;
            }
            status.error = true;
        }

        @Override
        public Object replace(ResolveStatus status, Object that, String name, Object value) {
            BeanModel model = BeanModel.typefor(that);
            if (model.properties().containsKey(name)) {
                return model.property(name).get().replace(that, value);
            }
            Iterator<IField> f = field(model, name);
            if (f.hasNext()) {
                return f.next().replace(that, value);
            }
            status.error = true;
            return null;
        }

        @Override
        public IClass<?> type(ResolveStatus status, IClass<?> that, String name) {
            BeanModel model = BeanModel.typeinfo(that);
            if (model.properties().containsKey(name)) {
                return model.property(name).get().type();
            }
            Iterator<IField> f = field(model, name);
            if (f.hasNext()) {
                return f.next().type();
            }
            status.error = true;
            return null;
        }
    };

    /**
     * Model for ResolveContext
     *
     * It will match if the object implements ResolveContext
     *
     * Every field will be accessed through context map first.
     * If map does not contain field, we use BEAN model to resolve it.
     *
     * ResolveContext properties are handled in specific way
     * if their type is ResolveFormat or ResolvePattern.
     * Those objects will be read and then evaluated against ResolveContext.
     */
    public static final ResolveModel CONTEXT = new ResolveModel() {
        @Override
        public boolean matches(Object that, String name) {
            return (that instanceof ResolveContext);
        }

        @Override
        public boolean matchesType(IClass<?> that, String _name) {
            return that.isSubclass(ResolveContext.class);
        }

        @Override
        public Object get(ResolveStatus status, Object that, String name) {
            Object result = ((ResolveContext) that).getResolveMap().get(name);

            if(result == null && BEAN.matches(that, name)) {
                result = BEAN.get(status, that, name);
            }

            if(result != null) {
                if(result instanceof ResolvePattern) {
                    ResolvePattern pattern = (ResolvePattern) result;
                    return pattern.get(that);
                }
                if(result instanceof ResolveFormat) {
                    ResolveFormat format = (ResolveFormat) result;
                    return format.format(that);
                }
                return result;
            } else {
                status.error = true;
                return null;
            }
        }

        @Override
        public void set(ResolveStatus _s, Object that, String name, Object value) {
            throw new ResolveException("ResolveContext is immutable.");
        }

        @Override
        public Object replace(ResolveStatus _s, Object that, String name, Object value) {
            throw new ResolveException("ResolveContext is immutable.");
        }

        @Override
        public IClass<?> type(ResolveStatus status, IClass<?> that, String name) {
            return IClass.OBJECT;
        }
    };

    /**
     * This class is simple wrapper which is used as "output parameter".
     *
     * Methods inside ResolveModel can set error flag to inform that property does not exist.
     */
    protected static final class ResolveStatus {

        public boolean error;

    }

    /**
     * This base abstract class can be used to write trivial ResolveModel which
     * supports only read-only view and does not support type deduction.
     *
     * You have to implement only "get" method which returns property value as optional.
     *
     * Please note that this model will consume objects of type T in every case, even if it can't find field.
     *
     * Please note that this model is unable to make type deducation and it will always return OBJECT.
     *
     * @param <T> T
     */
    public static abstract class AbstractView<T> extends ResolveModel {

        private final IClass<T> type;

        protected AbstractView(Class<T> type) {
            this.type = IClass.typeinfo(type);
        }

        /**
         * Method should be implemented by subclass.
         *
         * If property with provided name does not exist, should return optional.empty()
         *
         * @param that that
         * @param name name
         * @return optional
         */
        protected abstract Ref<Object> get(T that, String name);

        @Override
        public boolean matchesType(IClass<?> that, String name) {
            return type.isSuper(that);
        }

        @Override
        public boolean matches(Object that, String name) {
            return type.isInstance(that);
        }

        @Override
        public Object get(ResolveStatus status, Object that, String name) {
            if(type.isInstance(that)) {
                Ref<Object> out = get(type.cast(that), name);
                if(out.isPresent()) {
                    return out.get();
                }
            }
            status.error = true;
            return null;
        }

        @Override
        public IClass<?> type(ResolveStatus status, IClass<?> that, String name) {
            return IClass.OBJECT;
        }

        @Override
        public void set(ResolveStatus status, Object that, String name, Object value) {
            throw new ResolveException("Property " + name + " is immutable");
        }

        @Override
        public Object replace(ResolveStatus status, Object that, String name, Object value) {
            throw new ResolveException("Property " + name + " is immutable");
        }


    }
}
