package play.data.validation;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import net.sf.oval.ConstraintViolation;
import net.sf.oval.context.MethodParameterContext;
import net.sf.oval.guard.Guard;
import play.PlayPlugin;
import play.exceptions.ActionNotFoundException;
import play.exceptions.UnexpectedException;
import play.utils.Java;
import play.mvc.ActionInvoker;
import play.mvc.Http;
import play.mvc.Http.Cookie;
import play.mvc.Scope;
import play.mvc.results.Result;

public class ValidationPlugin extends PlayPlugin {
    
    static ThreadLocal<Map<Object,String>> keys = new ThreadLocal<Map<Object,String>>();

    @Override
    public void beforeInvocation() {
        keys.set(new HashMap<Object, String>());
        Validation.current.set(new Validation());
    }

    @Override
    public void beforeActionInvocation(Method actionMethod) {
        try {
            Validation.current.set(restore());
            boolean verify = false;
            for(Annotation[] annotations : actionMethod.getParameterAnnotations()) {
                if(annotations.length > 0) {
                    verify = true;
                    break;
                }
            }
            if(!verify) {
                return;
            }
            List<ConstraintViolation> violations = new Validator().validateAction(actionMethod);
            ArrayList<Error> errors = new ArrayList<Error>();
            String[] paramNames = Java.parameterNames(actionMethod);
            for (ConstraintViolation violation : violations) {
                errors.add(new Error(paramNames[((MethodParameterContext) violation.getContext()).getParameterIndex()], violation.getMessage(), violation.getMessageVariables() == null ? new String[0] : violation.getMessageVariables().values().toArray(new String[0])));
            }
            Validation.current.get().errors.addAll(errors);
        } catch (Exception e) {
            throw new UnexpectedException(e);
        }
    }

    @Override
    public void onActionInvocationResult(Result result) {
        save();
    }

    @Override
    public void onInvocationException(Throwable e) {
        clear();
    }

    @Override
    public void invocationFinally() {
        if(keys.get() != null) {
            keys.get().clear();
            keys.set(null);
        }
    }    

    
    // ~~~~~~

    static class Validator extends Guard {

        public List<ConstraintViolation> validateAction(Method actionMethod) throws Exception {
            List<ConstraintViolation> violations = new ArrayList<ConstraintViolation>();
            Object instance = null;
            // Patch for scala defaults
            if(!Modifier.isStatic(actionMethod.getModifiers()) && actionMethod.getDeclaringClass().getSimpleName().endsWith("$")) {
                try {
                    instance = actionMethod.getDeclaringClass().getDeclaredField("MODULE$").get(null);
                } catch(Exception e) {
                    throw new ActionNotFoundException(Http.Request.current().action, e);
                }
            }
            Object[] rArgs = ActionInvoker.getActionMethodArgs(actionMethod, instance);
            validateMethodParameters(null, actionMethod, rArgs, violations);
            validateMethodPre(null, actionMethod, rArgs, violations);
            return violations;
        }
    }
    
    static Pattern errorsParser = Pattern.compile("\u0000([^:]*):([^\u0000]*)\u0000");

    static Validation restore() {
        try {
            Validation validation = new Validation();
            Http.Cookie cookie = Http.Request.current().cookies.get(Scope.COOKIE_PREFIX + "_ERRORS");
            if (cookie != null) {
                String errorsData = URLDecoder.decode(cookie.value, "utf-8");
                Matcher matcher = errorsParser.matcher(errorsData);
                while (matcher.find()) {
                    String[] g2 = matcher.group(2).split("\u0001");
                    String message = g2[0];
                    String[] args = new String[g2.length - 1];
                    System.arraycopy(g2, 1, args, 0, args.length);
                    validation.errors.add(new Error(matcher.group(1), message, args));
                }
            }
            return validation;
        } catch (Exception e) {
            return new Validation();
        }
    }

    static void save() {
        try {
            StringBuilder errors = new StringBuilder();
            if(Validation.current() != null && Validation.current().keep) {
                for (Error error : Validation.errors()) {
                    errors.append("\u0000");
                    errors.append(error.key);
                    errors.append(":");
                    errors.append(error.message);
                    for(String variable : error.variables) {
                        errors.append("\u0001");
                        errors.append(variable);
                    }
                    errors.append("\u0000");
                }
            }
            String errorsData = URLEncoder.encode(errors.toString(), "utf-8");
            Http.Response.current().setCookie(Scope.COOKIE_PREFIX + "_ERRORS", errorsData);
        } catch (Exception e) {
            throw new UnexpectedException("Errors serializationProblem", e);
        }
    }     
    
    static void clear() {
        try {
            if(Http.Response.current() != null && Http.Response.current().cookies != null) {
                Cookie cookie = new Cookie();
                cookie.name = Scope.COOKIE_PREFIX + "_ERRORS";
                cookie.value = "";
                cookie.sendOnError = true;
                Http.Response.current().cookies.put(cookie.name, cookie);
            }
        } catch (Exception e) {
            throw new UnexpectedException("Errors serializationProblem", e);
        }
    } 
}
