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

package net.ranides.assira.text;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.type.TypeFactory;
import java.io.IOException;
import java.util.*;
import java.util.Map.Entry;
import net.ranides.assira.collection.ArrayUtils;
import net.ranides.assira.enums.EnumUtils;
import net.ranides.assira.generic.Function;
import net.ranides.assira.generic.TypeToken;
import net.ranides.assira.reflection.GenericClass;
import net.ranides.assira.time.DateQuickFormat;

/**
 * Klasa używana tylko do prezentowania obiektów jako tekst bardziej
 * reprezentatywnie niż za pomocą wbudowanych metod "toString".
 *
 * @author ranides
 */
public final class LexicalCast {

    private static final TypeFactory TYPE_FACTORY = TypeFactory.defaultInstance();
    private static final ObjectMapper MAPPER;

    static {
        MAPPER = new ObjectMapper();
        MAPPER.configure(SerializationFeature.INDENT_OUTPUT, true);
    }

    private LexicalCast() { }

    public static final Function<String, Object> SIMPLE =  new SimpleFunction();

    public static <T> List<String> asList(Iterable<T> values) {
        return asList(values, SIMPLE);
    }

    public static <T> List<String> asList(Iterable<T> values, Function<String,? super T> converter) {
        List<String> result = makeList(values);
        for(T value : values) { result.add( converter.apply(value) ); }
        return result;
    }

    public static <T> List<String> asList(T[] values) {
        return asList(ArrayUtils.wrap(values));
    }

    public static <T> List<String> asList(T[] values, Function<String,? super T> converter) {
        return asList(ArrayUtils.wrap(values), converter);
    }

    public static <K,V> List<String> asList(Map<K,V> map) {
        List<String> result = new ArrayList<>(map.size());
        for(Entry<K,V> entry : map.entrySet()) {
            result.add( SIMPLE.apply(entry.getKey()) + "=" + SIMPLE.apply(entry.getValue()) );
        }
        return result;
    }

    public static <K,V> List<String> asList(Map<K,V> map, Function<String,Entry<K,V>> converter) {
        List<String> result = new ArrayList<>(map.size());
        for(Entry<K,V> entry : map.entrySet()) {
            result.add( converter.apply(entry) );
        }
        return result;
    }

    private static <T> List<String> makeList(Iterable<T> values) {
        if( values instanceof Collection) {
            return new ArrayList<>( ((Collection)values).size() );
        } else {
            return new LinkedList<>();
        }
    }

    public static <T> String asText(T value) {
        return SIMPLE.apply(value);
    }

    public static String toJSON(Object value) throws LexicalCastException {
        try {
            return MAPPER.writeValueAsString(value);
        } catch(IOException cause) {
            throw new LexicalCastException(value.getClass()+" cannot be converted to JSON", cause);
        }
    }

    public static <T> T fromJSON(String value, Class<T> clazz) throws LexicalCastException {
        try {
            return MAPPER.readValue(value, clazz);
        } catch(IOException cause) {
            throw new LexicalCastException(clazz + " cannot be created from JSON", cause);
        }
    }
    
    @SuppressWarnings("unchecked")
    public static <T> T fromJSON(String value, TypeToken<T> token) throws LexicalCastException {
        return fromJSON(value, (GenericClass)token);
    }

    @SuppressWarnings("unchecked")
    public static <T> T fromJSON(String value, GenericClass token) throws LexicalCastException {
        try {
            return (T)MAPPER.readValue(value, TYPE_FACTORY.constructType( token.type() ) );
        } catch(IOException cause) {
            throw new LexicalCastException(token + " cannot be created from JSON", cause);
        }
    }

    @SuppressWarnings("unchecked")
    public static <T> T cast(Object value, Class<T> clazz) throws ClassCastException {
        return (T)rawCast(value, null, clazz);
    }
    
    @SuppressWarnings("unchecked")
    public static <T> T cast(Object value, TypeToken<T> token) throws LexicalCastException {
        return cast(value, (GenericClass)token);
    }

    @SuppressWarnings("unchecked")
    public static <T> T cast(Object value, GenericClass token) throws LexicalCastException {
        return (T)rawCast(value, token, TYPE_FACTORY.constructType( token.type() ).getRawClass());
    }

    @SuppressWarnings("PMD")
    private static Object rawCast(Object value, GenericClass token, Class<?> clazz) {
        // brak konwersji null
        if(value==null) {
            return null;
        }
        if(clazz.equals(value.getClass()) && clazz.getTypeParameters().length==0) {
            return value;
        }
        // konwersja EnumSet<?> -> String
        if( value instanceof EnumSet && clazz.equals(String.class) ) {
            return EnumUtils.encodeText((Set)value);
        }
        // konwersja Bool -> String
        if(value instanceof Boolean && clazz.equals(String.class) ) {
            return ((Boolean)value).toString();
        }
        // najpopularniejsze konwersje String -> Number / Bool / char[]
        if(value instanceof String) {
            // konwersja String -> Set<Enum<?>>
            if( EnumSet.class.isAssignableFrom(clazz) ) {
                return EnumUtils.decodeText((Class)token.params()[0].rawType(), (String)value );
            }
            if( clazz.equals(long.class) || clazz.equals(Long.class)) {
                return Long.valueOf((String)value);
            }
            if( clazz.equals(int.class) || clazz.equals(Integer.class)) {
                return Integer.valueOf((String)value);
            }
            if( clazz.equals(short.class) || clazz.equals(Short.class)) {
                return Short.valueOf((String)value);
            }
            if( clazz.equals(byte.class) || clazz.equals(Byte.class)) {
                return Integer.valueOf((String)value).byteValue();
            }
            if( clazz.equals(float.class) || clazz.equals(Float.class)) {
                return Float.valueOf((String)value);
            }
            if( clazz.equals(double.class) || clazz.equals(Double.class)) {
                return Double.valueOf((String)value);
            }
            if( clazz.equals(Boolean.class) ) {
                return Boolean.valueOf((String)value);
            }
        }
        // najpopularniejsze konwersje Number -> Number / String
        if(value instanceof Number) {
            if( clazz.equals(long.class) || clazz.equals(Long.class)) {
                return (Object)((Number)value).longValue();
            }
            if( clazz.equals(int.class) || clazz.equals(Integer.class)) {
                return (Object)((Number)value).intValue();
            }
            if( clazz.equals(short.class) || clazz.equals(Short.class)) {
                return (Object)((Number)value).shortValue();
            }
            if( clazz.equals(byte.class) || clazz.equals(Byte.class)) {
                return (Object)((Number)value).byteValue();
            }
            if( clazz.equals(float.class) || clazz.equals(Float.class)) {
                return (Object)((Number)value).floatValue();
            }
            if( clazz.equals(double.class) || clazz.equals(Double.class)) {
                return (Object)((Number)value).doubleValue();
            }
            if( clazz.equals(String.class) ) {
                return ((Number)value).toString();
            }
        }
        try {
            if(token == null) {
                return LexicalCast.fromJSON(LexicalCast.toJSON(value), clazz);
            } else {
                return LexicalCast.fromJSON(LexicalCast.toJSON(value), token);
            }
        } catch(LexicalCastException cause) {
            ClassCastException error = new ClassCastException(value + " cannot be cast to " + clazz);
            error.initCause(cause);
            throw error;
        }

    }

/* ************************************************************************** */

    private static class SimpleFunction implements Function<String, Object> {

        @SuppressWarnings({"PMD.NPathComplexity", "PMD.CyclomaticComplexity"})
        @Override
        public String apply(Object value) {
            if(value == null) {
                return "";
            }
            if( value instanceof String  || value instanceof Character ) {
                return StringCompiler.quote(value);
            }
            if( value instanceof Boolean || value instanceof Number ) {
                return value.toString();
            }
            if( value instanceof Date ) {
                return DateQuickFormat.format("date/long", value);
            }
            if( value instanceof Collection ) {
                return process((Collection)value);
            }
            if( value instanceof Map ) {
                return process( (Map<?,?>)value );
            }
            if(value instanceof Lexicable) {
                return ((Lexicable)value).toLexicalString();
            }
            if( value.getClass().isArray() ) {
                return process(ArrayUtils.wrap(value));
            }
            return value.toString();
        }
        
        private static <K,V> String process(Map<K,V> map) {
            return StringUtils.join(asList(map), "\n");
        }

        private static String process(Collection<?> values) {
            List<String> list = asList(values, new FromObject(){
                @Override
                public String apply(Object source) {
                    return " - " + SIMPLE.apply(source);
                }
            });
            return StringUtils.join(list, "\n");
        }


    }

    public static abstract class FromObject implements Function<String, Object> {
    }

    public static abstract class FromEntry<K,V> implements Function<String, Entry<K,V>> {

        public abstract String apply(K key, V value);

        @Override
        public String apply(Entry<K, V> source) {
            return apply(source.getKey(), source.getValue());
        }

    }

}
