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

package net.ranides.assira.text;

import java.io.IOException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.type.TypeFactory;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.temporal.TemporalAccessor;
import java.util.Date;
import net.ranides.assira.reflection.IClass;

/**
 * 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 = new ObjectMapper();

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

    private LexicalCast() {
        /* utility class */
    }

    public static String toJSON(Object value) throws ClassCastException {
        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> type) throws LexicalCastException {
        return fromJSON(value, IClass.typeinfo(type));
    }
    
    @SuppressWarnings("unchecked")
    public static <T> T fromJSON(String value, IClass<T> type) throws LexicalCastException {
        try {
            return (T)MAPPER.readValue(value, TYPE_FACTORY.constructType( type.reflective()) );
        } catch(IOException cause) {
            throw new LexicalCastException(type + " cannot be created from JSON", cause);
        }
    }

    public static <T> T cast(Object value, Class<T> clazz) throws ClassCastException {
        return cast(value, IClass.typeinfo(clazz));
    }
    
    @SuppressWarnings("unchecked")
    public static <T> T cast(Object value, IClass<T> type) throws LexicalCastException {
        return (T)rawcast(value, type);
    }

    @SuppressWarnings("PMD")
    private static Object rawcast(Object value, IClass<?> type) {
        // brak konwersji null
        if(value==null) {
            return null;
        }
        
        Class<?> raw = type.raw();
        
        if(raw.equals(value.getClass()) && raw.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 && raw.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( raw.equals(long.class) || raw.equals(Long.class)) {
                return Long.valueOf((String)value);
            }
            if( raw.equals(int.class) || raw.equals(Integer.class)) {
                return Integer.valueOf((String)value);
            }
            if( raw.equals(short.class) || raw.equals(Short.class)) {
                return Short.valueOf((String)value);
            }
            if( raw.equals(byte.class) || raw.equals(Byte.class)) {
                return Integer.valueOf((String)value).byteValue();
            }
            if( raw.equals(float.class) || raw.equals(Float.class)) {
                return Float.valueOf((String)value);
            }
            if( raw.equals(double.class) || raw.equals(Double.class)) {
                return Double.valueOf((String)value);
            }
            if( raw.equals(Boolean.class) ) {
                return Boolean.valueOf((String)value);
            }
        }
        // najpopularniejsze konwersje Number -> Number / String
        if(value instanceof Number) {
            if( raw.equals(long.class) || raw.equals(Long.class)) {
                return (Object)((Number)value).longValue();
            }
            if( raw.equals(int.class) || raw.equals(Integer.class)) {
                return (Object)((Number)value).intValue();
            }
            if( raw.equals(short.class) || raw.equals(Short.class)) {
                return (Object)((Number)value).shortValue();
            }
            if( raw.equals(byte.class) || raw.equals(Byte.class)) {
                return (Object)((Number)value).byteValue();
            }
            if( raw.equals(float.class) || raw.equals(Float.class)) {
                return (Object)((Number)value).floatValue();
            }
            if( raw.equals(double.class) || raw.equals(Double.class)) {
                return (Object)((Number)value).doubleValue();
            }
            if( raw.equals(String.class) ) {
                return ((Number)value).toString();
            }
            if( raw.isAssignableFrom(Date.class) ) {
                return new Date(((Number)value).longValue());
            }
        }
        if(value instanceof LocalDateTime) {
            LocalDateTime dt = (LocalDateTime)value;
            if( raw.equals(Date.class)) {
                return Date.from(dt.atZone(ZoneOffset.systemDefault()).toInstant());
            }
            if( raw.equals(java.sql.Date.class)) {
                return java.sql.Date.from(dt.atZone(ZoneOffset.systemDefault()).toInstant());
            }
        }
        if(value instanceof TemporalAccessor) {
            if( raw.equals(Date.class)) {
                return Date.from(Instant.from((TemporalAccessor)value));
            }
            if( raw.equals(java.sql.Date.class)) {
                return java.sql.Date.from(Instant.from((TemporalAccessor)value));
            }
        }
        if(value instanceof Date) {
            if( raw.isAssignableFrom(Instant.class) ) {
                return ((Date)value).toInstant();
            }
            if( raw.equals(long.class) || raw.equals(Long.class) || raw.isAssignableFrom(Long.class)) {
                return ((Date)value).getTime();
            }
        }
        if(!type.constructors().accepts(value).isEmpty()) {
            return type.construct(value);
        }
        return LexicalCast.fromJSON(LexicalCast.toJSON(value), type);
    }
    
    private static final class LexicalCastException extends ClassCastException {
        
        private static final long serialVersionUID = 1L;

        public LexicalCastException(String message, Throwable cause) {
            super(message);
            initCause(cause);
        }
        
    }

}
