package act.view;

/*-
 * #%L
 * ACT Framework
 * %%
 * Copyright (C) 2014 - 2017 ActFramework
 * %%
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * #L%
 */

import static act.util.ActError.Util.errorMessage;
import static act.util.ActError.Util.loadSourceInfo;
import static org.osgl.http.H.Status.INTERNAL_SERVER_ERROR;

import act.Act;
import act.app.*;
import act.asm.AsmContext;
import act.asm.AsmException;
import act.exception.BindException;
import act.util.ActError;
import org.osgl.$;
import org.osgl.exception.*;
import org.osgl.http.H;
import org.osgl.mvc.annotation.ResponseStatus;
import org.osgl.mvc.result.ErrorResult;
import org.osgl.mvc.result.Result;
import org.osgl.util.E;
import org.osgl.util.S;

import java.util.*;
import javax.persistence.PersistenceException;
import javax.validation.*;

public class ActErrorResult extends ErrorResult implements ActError {

    protected SourceInfo sourceInfo;

    public ActErrorResult(H.Status status) {
        super(status, errorMessage(status));
        init();
        populateSourceInfo();
    }

    public ActErrorResult(H.Status status, String message, Object... args) {
        super(status, errorMessage(status, message, args));
        init();
        populateSourceInfo();
    }

    public ActErrorResult(H.Status status, int errorCode) {
        super(status, errorCode, errorMessage(status));
        init();
        populateSourceInfo();
    }

    public ActErrorResult(H.Status status, int errorCode, String message, Object... args) {
        super(status, errorCode, errorMessage(status, message, args));
        init();
        populateSourceInfo();
    }

    public ActErrorResult(Throwable cause) {
        super(INTERNAL_SERVER_ERROR, cause, errorMessage(INTERNAL_SERVER_ERROR));
        init();
        populateSourceInfo(cause);
    }

    private ActErrorResult(AsmException exception, boolean scanning) {
        super(INTERNAL_SERVER_ERROR, exception.getCause(), errorMsg(exception, scanning));
        init();
        populateSourceInfo(exception.context());
    }

    public ActErrorResult(H.Status status, Throwable cause) {
        super(status, cause, errorMessage(status));
        init();
        populateSourceInfo(cause);
    }

    public ActErrorResult(H.Status status, Throwable cause, String message, Object... args) {
        super(status, cause, errorMessage(status, message, args));
        init();
        populateSourceInfo(cause);
    }

    public ActErrorResult(H.Status status, int errorCode, Throwable cause, String message, Object... args) {
        super(status, errorCode, cause, errorMessage(status, message, args));
        init();
        populateSourceInfo(cause);
    }

    public ActErrorResult(H.Status status, int errorCode, Throwable cause) {
        super(status, errorCode, cause, errorMessage(status));
        init();
        populateSourceInfo(cause);
    }

    @Override
    public Throwable getCauseOrThis() {
        Throwable cause = getCause();
        return null == cause ? this : cause;
    }

    public SourceInfo sourceInfo() {
        return sourceInfo;
    }

    @Override
    public int statusCode() {
        Throwable cause = super.getCause();
        int statusCode = null == cause ? -1 : userDefinedStatusCode(cause.getClass());
        return -1 == statusCode ? super.statusCode() : statusCode;
    }

    public List<String> stackTrace() {
        return Util.stackTraceOf(this);
    }

    @Override
    public boolean isErrorSpot(String traceLine, String nextTraceLine) {
        return false;
    }

    protected void init() {
    }

    protected void populateSourceInfo(Throwable cause) {
        if (!Act.isDev()) {
            return;
        }
        sourceInfo = loadSourceInfo(cause, getClass());
    }

    private void populateSourceInfo() {
        if (!Act.isDev()) {
            return;
        }
        sourceInfo = loadSourceInfo(new RuntimeException(), getClass());
    }

    protected void populateSourceInfo(AsmContext context) {
        if (!Act.isDev()) {
            return;
        }
        this.sourceInfo = Util.loadSourceInfo(context);
    }

    private static Map<Class<? extends Throwable>, $.Function<Throwable, Result>> x = new HashMap<>();

    static {
        $.Function<Throwable, Result> unsupported = new $.Transformer<Throwable, Result>() {
            @Override
            public Result transform(Throwable throwable) {
                Act.LOGGER.warn(throwable, "Error:");
                return ActNotImplemented.create(throwable);
            }
        };
        x.put(ToBeImplemented.class, new $.Transformer<Throwable, Result>() {
            @Override
            public Result transform(Throwable throwable) {
                Act.LOGGER.warn(throwable, "Error:");
                return ActToBeImplemented.create();
            }
        });
        x.put(UnsupportedException.class, unsupported);
        x.put(UnsupportedOperationException.class, unsupported);
        x.put(IllegalStateException.class, new $.Transformer<Throwable, Result>() {
            @Override
            public Result transform(Throwable throwable) {
                Act.LOGGER.warn(throwable, "Error:");
                return ActConflict.create(throwable);
            }
        });
        x.put(ResourceNotFoundException.class, new $.Transformer<Throwable, Result>() {
            @Override
            public Result transform(Throwable throwable) {
                Act.LOGGER.warn(throwable, "Error:");
                return ActNotFound.create(throwable);
            }
        });
        x.put(AccessDeniedException.class, new $.Transformer<Throwable, Result>() {
            @Override
            public Result transform(Throwable throwable) {
                Act.LOGGER.warn(throwable, "Error:");
                return ActForbidden.create(throwable);
            }
        });
        $.Transformer<Throwable, Result> badRequest = new $.Transformer<Throwable, Result>() {
            @Override
            public Result transform(Throwable throwable) {
                Act.LOGGER.warn(throwable, "Error:");
                return ActBadRequest.create(throwable);
            }
        };
//        x.put(IllegalArgumentException.class, badRequest);
//        x.put(InvalidRangeException.class, badRequest);
//        x.put(IndexOutOfBoundsException.class, badRequest);
        x.put(ValidationException.class, badRequest);
        x.put(BindException.class, badRequest);
    }

    private static Map<Class, Integer> userDefinedStatus;

    public static void classInit(App app) {
        userDefinedStatus = app.createMap();
    }

    private static int userDefinedStatusCode(Class<? extends Throwable> exCls) {
        Integer I = userDefinedStatus.get(exCls);
        if (null == I) {
            ResponseStatus rs = exCls.getAnnotation(ResponseStatus.class);
            if (null == rs) {
                I = -1;
                userDefinedStatus.put(exCls, -1);
            } else {
                I = rs.value();
            }
        }
        return I;
    }

    public static Result of(Throwable t) {
        if (t instanceof Result) {
            return (Result) t;
        }
        if (t instanceof PersistenceException && null != t.getCause()) {
            t = t.getCause();
        }
        if (t instanceof ConstraintViolationException) {
            ConstraintViolationException e = (ConstraintViolationException) t;
            Set<ConstraintViolation<?>> violations = e.getConstraintViolations();
            Map<String, ConstraintViolation> lookup = new HashMap<>();
            for (ConstraintViolation v : violations) {
                lookup.put(S.pathConcat(v.getRootBeanClass().getSimpleName(), '.', v.getPropertyPath().toString()), v);
            }
            S.Buffer buf = S.buffer();
            ActionContext.buildViolationMessage(lookup, buf, ";");
            String msg = buf.toString();
            return ActBadRequest.create(msg);
        } else if (t instanceof org.rythmengine.exception.RythmException) {
            return Act.isDev() ? new RythmTemplateException((org.rythmengine.exception.RythmException) t) : ErrorResult.of(INTERNAL_SERVER_ERROR);
        } else if (t instanceof AsmException) {
            return new ActErrorResult((AsmException) t, true);
        }
        $.Function<Throwable, Result> transformer = transformerOf(t);
        return null == transformer ? new ActErrorResult(t) : transformer.apply(t);
    }

    public static ActErrorResult scanningError(AsmException exception) {
        return new ActErrorResult(exception, true);
    }

    public static ActErrorResult enhancingError(AsmException exception) {
        return new ActErrorResult(exception, false);
    }

    private static $.Function<Throwable, Result> transformerOf(Throwable t) {
        Class tc = t.getClass();
        $.Function<Throwable, Result> transformer = x.get(tc);
        if (null != transformer) {
            return transformer;
        }
        for (Class c : x.keySet()) {
            if (c.isAssignableFrom(tc)) {
                return x.get(c);
            }
        }
        return null;
    }

    public static Result ofStatus(int statusCode) {
        return of(H.Status.of(statusCode));
    }

    @Deprecated
    public static Result of(int statusCode) {
        E.illegalArgumentIf(statusCode < 400);
        switch (statusCode) {
            case 400:
                return ActBadRequest.create();
            case 401:
                return ActUnauthorized.create();
            case 403:
                return ActForbidden.create();
            case 404:
                return ActNotFound.create();
            case 405:
                return ActMethodNotAllowed.create();
            case 409:
                return ActConflict.create();
            case 501:
                return ActNotImplemented.create();
            default:
                if (Act.isDev()) {
                    return new ActErrorResult(new RuntimeException());
                } else {
                    return new ErrorResult(H.Status.of(statusCode));
                }
        }
    }

    public static ErrorResult of(H.Status status) {
        E.illegalArgumentIf(!status.isClientError() && !status.isServerError());
        if (Act.isDev()) {
            return new ActErrorResult(status);
        } else {
            return ErrorResult.of(status);
        }
    }

    public static ErrorResult of(H.Status status, String message, Object... args) {
        E.illegalArgumentIf(!status.isClientError() && !status.isServerError());
        if (Act.isDev()) {
            return new ActErrorResult(status, message, args);
        } else {
            return ErrorResult.of(status, message, args);
        }
    }

    public static ErrorResult of(H.Status status, int errorCode) {
        E.illegalArgumentIf(!status.isClientError() && !status.isServerError());
        if (Act.isDev()) {
            return new ActErrorResult(status, errorCode);
        } else {
            return ErrorResult.of(status, errorCode);
        }
    }

    public static ErrorResult of(H.Status status, int errorCode, String message, Object... args) {
        E.illegalArgumentIf(!status.isClientError() && !status.isServerError());
        if (Act.isDev()) {
            return new ActErrorResult(status, errorCode, message, args);
        } else {
            return ErrorResult.of(status, errorCode, message, args);
        }
    }

    public static Throwable rootCauseOf(Throwable t) {
        if (null == t) {
            return null;
        }
        Throwable cause;
        for (; ; ) {
            cause = t.getCause();
            if (null != cause) {
                t = cause;
            } else {
                break;
            }
        }
        return t;
    }


    private static String errorMsg(AsmException exception, boolean scanning) {
        String userMsg = exception.getLocalizedMessage();
        return (S.blank(userMsg)) ? S.concat("Error ", scanning ? "scanning" : "enhancing", " bytecode at ", exception.context().toString()) : userMsg;
    }
}
