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

package net.ranides.assira.text;

import lombok.experimental.UtilityClass;

import net.ranides.assira.reflection.*;

import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.temporal.TemporalAccessor;
import java.util.Date;

/**
 * This class is used to cast objects using their values, not inheritance tree.
 *
 * Essentially, it works like you converted "value" into text and then parsed into target "clazz".
 *
 * Supports numbers, strings, boolean, dates, and objects constructible from String.
 *
 * @author ranides
 */
@UtilityClass
public final class LexicalCast {

    /**
     * Converts value into specified clazz.
     *
     * If target type is not built-in, it converts value using "toString" and
     * constructs result passing this generated string to constructor.
     *
     * @param value value
     * @param clazz clazz
     * @param <T> T
     * @return T
     * @throws LexicalCastException if conversion is not supported
     */
    public static <T> T cast(Object value, Class<T> clazz) throws LexicalCastException {
        return cast(value, IClass.typeinfo(clazz));
    }

    /**
     * Converts value into specified clazz.
     *
     * If target type is not built-in, it converts value using "toString" and
     * constructs result passing this generated string to constructor.
     *
     * @param value value
     * @param type type
     * @param <T> T
     * @return T
     * @throws LexicalCastException if conversion is not supported
     */
    @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) || raw.equals(Boolean.class) ) {
                if("true".equalsIgnoreCase((String)value)) {
                    return true;
                }
                if("false".equalsIgnoreCase((String)value)) {
                    return false;
                }
                throw new LexicalCastException(value + " cannot be converted to boolean");
            }
        }
        // najpopularniejsze konwersje Number -> Number / String
        if(value instanceof Number) {
            if( raw.equals(long.class) || raw.equals(Long.class)) {
                return ((Number)value).longValue();
            }
            if( raw.equals(int.class) || raw.equals(Integer.class)) {
                return ((Number)value).intValue();
            }
            if( raw.equals(short.class) || raw.equals(Short.class)) {
                return ((Number)value).shortValue();
            }
            if( raw.equals(byte.class) || raw.equals(Byte.class)) {
                return ((Number)value).byteValue();
            }
            if( raw.equals(float.class) || raw.equals(Float.class)) {
                return ((Number)value).floatValue();
            }
            if( raw.equals(double.class) || raw.equals(Double.class)) {
                return ((Number)value).doubleValue();
            }
            if( raw.equals(String.class) ) {
                return 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);
        }

        throw new LexicalCastException(value + " cannot be converted to " + type);
    }

    private static final class LexicalCastException extends ClassCastException {
        
        private static final long serialVersionUID = 1L;

        public LexicalCastException(String message) {
            super(message);
        }
        
    }

}
