/*
 * Copyright (C) 2013 Red Hat, Inc. and/or its affiliates.
 *
 * 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.
 */

package org.jboss.errai.ioc.rebind.ioc.extension.builtin;

import static org.jboss.errai.codegen.meta.MetaClassFactory.parameterizedAs;
import static org.jboss.errai.codegen.meta.MetaClassFactory.typeParametersOf;
import static org.jboss.errai.codegen.util.Stmt.castTo;
import static org.jboss.errai.codegen.util.Stmt.declareFinalVariable;
import static org.jboss.errai.codegen.util.Stmt.invokeStatic;
import static org.jboss.errai.codegen.util.Stmt.nestedCall;
import static org.jboss.errai.codegen.util.Stmt.newObject;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;

import org.jboss.errai.codegen.Parameter;
import org.jboss.errai.codegen.Statement;
import org.jboss.errai.codegen.builder.AnonymousClassStructureBuilder;
import org.jboss.errai.codegen.builder.BlockBuilder;
import org.jboss.errai.codegen.builder.ContextualStatementBuilder;
import org.jboss.errai.codegen.builder.impl.ObjectBuilder;
import org.jboss.errai.codegen.exception.GenerationException;
import org.jboss.errai.codegen.meta.MetaClass;
import org.jboss.errai.codegen.meta.MetaClassFactory;
import org.jboss.errai.codegen.meta.MetaMethod;
import org.jboss.errai.codegen.meta.MetaParameter;
import org.jboss.errai.codegen.util.GenUtil;
import org.jboss.errai.codegen.util.Refs;
import org.jboss.errai.ioc.client.api.CodeDecorator;
import org.jboss.errai.ioc.client.api.UncaughtExceptionHandler;
import org.jboss.errai.ioc.client.container.ErraiUncaughtExceptionHandler;
import org.jboss.errai.ioc.client.container.Factory;
import org.jboss.errai.ioc.rebind.ioc.extension.IOCDecoratorExtension;
import org.jboss.errai.ioc.rebind.ioc.injector.api.Decorable;
import org.jboss.errai.ioc.rebind.ioc.injector.api.FactoryController;

import com.google.gwt.core.client.GWT;

/**
 * Generates factory/instance initialization statements to register {@link UncaughtExceptionHandler} methods with the
 * {@link ErraiUncaughtExceptionHandler}.
 *
 * @author Christian Sadilek <csadilek@redhat.com>
 * @author Max Barkley <mbarkley@redhat.com>
 */
@CodeDecorator
public class UncaughtExceptionDecorator extends IOCDecoratorExtension<UncaughtExceptionHandler> {

  public UncaughtExceptionDecorator(final Class<UncaughtExceptionHandler> decoratesWith) {
    super(decoratesWith);
  }

  @Override
  public void generateDecorator(final Decorable decorable, final FactoryController controller) {
    final MetaMethod method = validateExceptionHandlingMethod(decorable);

    final boolean enclosingTypeDependent = decorable.isEnclosingTypeDependent();
    final String handlerVar = method.getName() + "Handler";
    final String cleanupRunnableVar = method.getName() + "HandlerCleanup";
    final Statement setCleanupRefStmt = controller.setReferenceStmt(cleanupRunnableVar, Refs.get(cleanupRunnableVar));
    final Statement getCleanupRefStmt = controller.getReferenceStmt(cleanupRunnableVar, Runnable.class);

    final List<Statement> initStmts = new ArrayList<>(Arrays.asList(
            declareAndInitHandlerVar(decorable, controller, handlerVar, enclosingTypeDependent),
            declareFinalVariable(cleanupRunnableVar, Runnable.class, addHandler(handlerVar))));

    if (enclosingTypeDependent) {
      initStmts.add(setCleanupRefStmt);
    }

    final List<Statement> destructionStmts = Collections.singletonList(nestedCall(getCleanupRefStmt).invoke("run"));

    if (enclosingTypeDependent) {
      controller.addInitializationStatements(initStmts);
      controller.addDestructionStatements(destructionStmts);
    } else {
      controller.addFactoryInitializationStatements(initStmts);
    }
  }

  private ContextualStatementBuilder addHandler(final String handlerVar) {
    return castTo(ErraiUncaughtExceptionHandler.class, invokeStatic(GWT.class, "getUncaughtExceptionHandler"))
            .invoke("addHandler", Refs.get(handlerVar));
  }

  private MetaMethod validateExceptionHandlingMethod(final Decorable decorable) {
    // Ensure that method has exactly one parameter of type Throwable
    final MetaMethod method = decorable.getAsMethod();
    final MetaParameter[] parms = method.getParameters();
    if (!(parms.length == 1 && parms[0].getType().equals(MetaClassFactory.get(Throwable.class)))) {
      throw new GenerationException("Methods annotated with " + UncaughtExceptionHandler.class.getName()
          + " must have exactly one parameter of type " + Throwable.class.getName()
          + ". Invalid parameters in method: "
          + GenUtil.getMethodString(method) + " of type " + method.getDeclaringClass() + ".");
    }
    return method;
  }

  private Statement declareAndInitHandlerVar(final Decorable decorable, final FactoryController controller,
          final String name, final boolean enclosingTypeDependent) {
    final MetaClass throwableConsumerClass = parameterizedAs(Consumer.class, typeParametersOf(Throwable.class));
    final BlockBuilder<AnonymousClassStructureBuilder> initBuilder = newObject(throwableConsumerClass)
            .extend().publicOverridesMethod("accept", Parameter.of(Throwable.class, "t"));
    if (!enclosingTypeDependent) {
      final MetaClass enclosingType = decorable.getEnclosingInjectable().getInjectedType();
      initBuilder.append(declareFinalVariable("instance", enclosingType,
              castTo(enclosingType, invokeStatic(Factory.class, "maybeUnwrapProxy", controller.contextGetInstanceStmt()))));
    }

    final ObjectBuilder initStmt = initBuilder.append(decorable.call(Refs.get("t"))).finish().finish();
    final Statement handlerStatement = declareFinalVariable(name, throwableConsumerClass, initStmt);

    return handlerStatement;
  }
}
