package com.rjconsultores.ventaboletos.web.utilerias.render;

import java.beans.IntrospectionException;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;

import javax.persistence.Temporal;

import org.zkoss.zul.Listcell;
import org.zkoss.zul.Listitem;
import org.zkoss.zul.ListitemRenderer;

import com.rjconsultores.ventaboletos.anotacao.Renderizado;

/**
 * Implementa um renderizador padrão para itens de lista, usando a anotação {@link Renderizado} 
 * para determinar quais campos renderizar e em qual ordem.
 *
 * @param <E> O tipo de objeto a ser renderizado.
 */
public class RenderPadrao<E> implements ListitemRenderer {
    
    private Class<E> custom;
    private Listcell lc;

    /**
     * Constrói uma instância do renderizador com a classe especificada.
     *
     * @param classe A classe do tipo de objeto a ser renderizado.
     */
    public RenderPadrao(Class<E> classe) {
        this.custom = classe;
    }

    /**
     * Renderiza um item de lista com base nos campos anotados com {@link Renderizado} da classe do objeto.
     *
     * @param lstm O item de lista a ser renderizado.
     * @param o O objeto a ser renderizado.
     * @throws Exception Se ocorrer algum erro durante a renderização.
     */
    @Override
    public void render(Listitem lstm, Object o) throws Exception {
        E cast = convertToE(o);
        Field[] campos = cast.getClass().getDeclaredFields();
        List<Field> camposOrdenados = new ArrayList<Field>();
        
        for (Field campo : campos) {
            if (campo.isAnnotationPresent(Renderizado.class)) {
                camposOrdenados.add(campo);
            }
        }

        ordenacao(camposOrdenados);
        
        for (Field campo : camposOrdenados) {
            lc = new Listcell(obterValorCampo(cast, campo).toString());
            lc.setParent(lstm);    		
        }  
        
        lstm.setAttribute("data", cast);
    }

    /**
     * Ordena os campos anotados com {@link Renderizado} com base na posição definida na anotação.
     *
     * @param camposOrdenados A lista de campos a ser ordenada.
     */
    private void ordenacao(List<Field> camposOrdenados) {
        Collections.sort(camposOrdenados, new Comparator<Field>() {
            public int compare(Field f1, Field f2) {
                Renderizado r1 = f1.getAnnotation(Renderizado.class);
                Renderizado r2 = f2.getAnnotation(Renderizado.class);
                return Integer.compare(r1.posicao(), r2.posicao());
            }
        });
    }
    
    /**
     * Converte o objeto fornecido para o tipo especificado na criação do renderizador.
     *
     * @param o O objeto a ser convertido.
     * @return O objeto convertido para o tipo especificado.
     * @throws IllegalArgumentException Se o objeto não for do tipo esperado.
     */
    public E convertToE(Object o) {
        if (o == null || !custom.isInstance(o)) {
            throw new IllegalArgumentException("O objeto não é do tipo esperado: " + custom.getName());
        }
        return (E) o;
    }
    
    /**
     * Obtém o valor de um campo de um objeto e aplica a conversão definida na anotação {@link Renderizado}.
     *
     * @param objeto O objeto do qual o valor do campo deve ser obtido.
     * @param campo O campo do qual o valor deve ser obtido.
     * @return O valor do campo convertido conforme definido na anotação.
     * @throws IntrospectionException Se ocorrer um erro ao introspectar o campo.
     * @throws IllegalAccessException Se ocorrer um erro ao acessar o campo.
     * @throws IllegalArgumentException Se ocorrer um erro ao passar argumentos ao método.
     * @throws InvocationTargetException Se ocorrer um erro ao invocar o método. 
     * @throws NoSuchMethodException Se ocorrer um erro ao encontrar o método.
     * @throws SecurityException Se ocorrer um erro de segurança ao acessar o método.
     */
    private static Object obterValorCampo(Object objeto, Field campo) 
            throws IntrospectionException, IllegalAccessException, InvocationTargetException, 
                   IllegalArgumentException, NoSuchMethodException, SecurityException  {        
        PropertyDescriptor pd = new PropertyDescriptor(campo.getName(), objeto.getClass());

        Method getter = pd.getReadMethod();
        
        if (getter != null) {
        	Object raw = getter.invoke(objeto);
        	
        	if(raw == null) {
        		return "";
        	}
        	
            String valor = raw.toString();
            Renderizado renderizado = campo.getAnnotation(Renderizado.class);
            Class<? extends Enum<?>> conversor = renderizado.conversor();
            
            if (conversor == Renderizado.DefaultEnum.class) {
                if (campo.getType().equals(Date.class)) {
                    return trataData(objeto, campo, getter);
                }
                return valor;
            } else {
                try {
                    String nomeConversor = renderizado.metodoConversor();
                    Method metodoConversor = conversor.getMethod(nomeConversor, String.class);
                    Enum<?> enumResultado = (Enum<?>) metodoConversor.invoke(null, valor);
                    return enumResultado.toString();
                } catch (NoSuchMethodException e) {
                    throw new NoSuchMethodException("Classe conversora precisa implementar o metodo 'buscarPeloValor' " + conversor.getName());
                } catch (Exception e) {
                    return valor;
                }
            }
            
        } else {
            throw new IllegalArgumentException("Getter não encontrado para o campo: " + campo.getName());
        }
    }

    /**
     * Formata data obtida de um campo, aplicando o formato especificado na anotação {@link Temporal}.
     *
     * @param objeto O objeto do qual o valor da data deve ser obtido.
     * @param campo O campo do qual a data deve ser obtida.
     * @param getter O método getter para obter o valor da data.
     * @return A data formatada dd/MM/yyyy, se tiver tambem anotação {@link Temporal} retorna dd/MM/yyyy HH:mm:ss.
     * @throws IllegalAccessException Se ocorrer um erro ao acessar o campo.
     * @throws InvocationTargetException Se ocorrer um erro ao invocar o método getter.
     */
    private static Object trataData(Object objeto, Field campo, Method getter) 
            throws IllegalAccessException, InvocationTargetException {
        SimpleDateFormat formatter = new SimpleDateFormat("dd/MM/yyyy");
        if (campo.isAnnotationPresent(Temporal.class)) {
            formatter = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss");
        }
        return formatter.format(getter.invoke(objeto));
    }

    /**
     * Obtém o último {@link Listcell} criado durante o processo de renderização.
     *
     * @return O último {@link Listcell} criado.
     */
    public Listcell getLc() {
        return lc;
    }
}
