package example.common.util;

import java.lang.reflect.ParameterizedType;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import example.common.exception.SystemException;

public abstract class GenericTemplate<T, E extends Exception> {

    private final String name;

    private final Object[] args;

    private final Class<? extends Exception> throwableClass;

    private T returning = null;

    private Throwable throwing = null;

    static final Logger logger = LoggerFactory.getLogger(GenericTemplate.class);

    public GenericTemplate(String name, Object... args) {
        this.name = name;
        this.args = args;
        this.throwableClass = getThrowableClass0();
    }

    public GenericTemplate(String name,
            Class<? extends Exception> throwableClass, Object... args) {
        this.name = name;
        this.args = args;
        this.throwableClass = throwableClass;
    }

    @SuppressWarnings("unchecked")
    private Class<? extends Exception> getThrowableClass0() {
        for (Class<?> c = getClass(); c != Object.class; c = c.getSuperclass()) {
            if (c.getSuperclass() == GenericTemplate.class) {
                ParameterizedType type = ParameterizedType.class
                        .cast(c.getGenericSuperclass());
                return Class.class.cast(type.getActualTypeArguments()[1]);
            }
        }
        return RuntimeException.class;
    }

    protected String getName() {
        return name;
    }

    protected Object[] getArgs() {
        return args;
    }

    protected Object getArg(int index) {
        return args[index];
    }

    protected String getSignature() {
        StringBuilder sb = new StringBuilder();
        sb.append(getName());
        sb.append('(');
        if (args.length > 0) {
            appendObject(sb, args[0]);
            for (int i = 1; i < args.length; ++i) {
                sb.append(", ");
                appendObject(sb, args[i]);
            }
        }
        sb.append(')');
        return sb.toString();
    }

    protected Class<? extends Exception> getThrowableClass() {
        return throwableClass;
    }

    protected T getReturning() {
        return returning;
    }

    protected T setReturning(T returning) {
        return this.returning = returning;
    }

    protected Throwable getThrowing() {
        return throwing;
    }

    // TODO 本当は throws E としたい。
    public T invoke() throws Exception {
        try {
            return doInvoke();
        } catch (Throwable t) {
            throw toThrowableException(t);
        }
    }

    public T doInvoke() throws Throwable {
        String signature = null;
        if (logger.isDebugEnabled()) {
            signature = getSignature();
            logger.debug("BEFORE " + signature);
        }
        before();
        
        try {
            if (logger.isDebugEnabled()) {
                logger.debug("EXECUTE " + signature);
            }
            execute();
            
            if (logger.isDebugEnabled()) {
                logger.debug("RETURNING(" + getName() + ") => " + getReturning());
            }
            afterReturning();
            return returning;
            
        } catch (Throwable t) {
            throwing = t;
            if (logger.isDebugEnabled()) {
                logger.debug("THROWING(" + getName() + ") => " + getThrowing());
            }
            afterThrowing();
            throw throwing;
            
        } finally {
            try {
                if (logger.isDebugEnabled()) {
                    logger.debug("AFTER " + signature);
                }
                after();
                
            } catch (Exception e) {
                if (throwing == null || !throwableClass.isInstance(e)) {
                    throw e;
                }
                // after() で発生した例外が throws で指定した例外の場合は、
                // execute() で発生した例外を優先。
                // close() などの実行を想定。
                logger.warn("thrown at after(" + getName() + ")", e);
            }
        }
    }

    protected void before() throws Throwable {}

    protected abstract void execute() throws Throwable;

    protected void afterReturning() throws Throwable {}

    protected void afterThrowing() throws Throwable {}

    protected void after() throws Throwable {}

    protected void appendObject(StringBuilder sb, Object value) {
        sb.append(value);
    }

    @SuppressWarnings("unchecked")
    protected E toThrowableException(Throwable t) {
        if (t instanceof Error) {
            throw (Error) t;
        } else if (t instanceof RuntimeException) {
            throw (RuntimeException) t;
        } else if (getThrowableClass().isInstance(t)) {
            return (E) t;
        } else {
            throw new SystemException(t);
        }
    }

}
