/*
 * 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.databinding.rebind;

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.declareFinalVariable;
import static org.jboss.errai.codegen.util.Stmt.invokeStatic;
import static org.jboss.errai.codegen.util.Stmt.loadVariable;

import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

import javax.enterprise.context.Dependent;

import org.jboss.errai.codegen.Statement;
import org.jboss.errai.codegen.builder.ClassStructureBuilder;
import org.jboss.errai.codegen.meta.HasAnnotations;
import org.jboss.errai.codegen.meta.MetaClass;
import org.jboss.errai.databinding.client.api.DataBinder;
import org.jboss.errai.ioc.client.api.IOCExtension;
import org.jboss.errai.ioc.client.container.RefHolder;
import org.jboss.errai.ioc.rebind.ioc.bootstrapper.AbstractBodyGenerator;
import org.jboss.errai.ioc.rebind.ioc.bootstrapper.FactoryBodyGenerator;
import org.jboss.errai.ioc.rebind.ioc.bootstrapper.IOCProcessingContext;
import org.jboss.errai.ioc.rebind.ioc.extension.IOCExtensionConfigurator;
import org.jboss.errai.ioc.rebind.ioc.graph.api.CustomFactoryInjectable;
import org.jboss.errai.ioc.rebind.ioc.graph.api.DependencyGraph;
import org.jboss.errai.ioc.rebind.ioc.graph.api.DependencyGraphBuilder.InjectableType;
import org.jboss.errai.ioc.rebind.ioc.graph.api.Injectable;
import org.jboss.errai.ioc.rebind.ioc.graph.api.InjectionSite;
import org.jboss.errai.ioc.rebind.ioc.graph.impl.DefaultCustomFactoryInjectable;
import org.jboss.errai.ioc.rebind.ioc.graph.impl.FactoryNameGenerator;
import org.jboss.errai.ioc.rebind.ioc.graph.impl.InjectableHandle;
import org.jboss.errai.ioc.rebind.ioc.injector.api.InjectableProvider;
import org.jboss.errai.ioc.rebind.ioc.injector.api.InjectionContext;
import org.jboss.errai.ioc.rebind.ioc.injector.api.WiringElementType;
import org.jboss.errai.ui.shared.api.annotations.Model;

/**
 * The purpose of this IOC extension is to provide bean instances of bindable types that are qualified with
 * {@link Model} and to expose the {@link DataBinder}s that manage these model instances using {@link RefHolder} s.
 *
 * @author Christian Sadilek <csadilek@redhat.com>
 * @author Mike Brock
 */
@IOCExtension
public class DataBindingIOCExtension implements IOCExtensionConfigurator {

  @Override
  public void configure(final IOCProcessingContext context, final InjectionContext injectionContext) {
  }

  @Override
  public void afterInitialization(final IOCProcessingContext context, final InjectionContext injectionContext) {

    final Collection<MetaClass> allBindableTypes = DataBindingUtil.getAllBindableTypes(context.getGeneratorContext());
    final Model anno = new Model() {
      @Override
      public Class<? extends Annotation> annotationType() {
        return Model.class;
      }
    };

    for (final MetaClass modelBean : allBindableTypes) {
      final InjectableHandle handle = new InjectableHandle(modelBean,
              injectionContext.getQualifierFactory().forSource(new HasAnnotations() {

                @Override
                public boolean isAnnotationPresent(Class<? extends Annotation> annotation) {
                  return Model.class.equals(annotation);
                }

                @Override
                public Annotation[] getAnnotations() {
                  return new Annotation[] {
                      anno
                  };
                }

                @SuppressWarnings("unchecked")
                @Override
                public <A extends Annotation> A getAnnotation(Class<A> annotation) {
                  if (isAnnotationPresent(annotation)) {
                    return (A) anno;
                  }
                  else {
                    return null;
                  }
                }
              }));

      injectionContext.registerInjectableProvider(handle, new InjectableProvider() {

        private CustomFactoryInjectable provided;

        @Override
        public CustomFactoryInjectable getInjectable(final InjectionSite injectionSite, final FactoryNameGenerator nameGenerator) {
          if (injectionSite.isAnnotationPresent(Model.class)) {
            if (provided == null) {
              final FactoryBodyGenerator generator = new AbstractBodyGenerator() {

                @Override
                protected List<Statement> generateCreateInstanceStatements(
                        final ClassStructureBuilder<?> bodyBlockBuilder, final Injectable injectable,
                        final DependencyGraph graph, final InjectionContext injectionContext) {
                  final List<Statement> createInstanceStmts = new ArrayList<Statement>();
                  final MetaClass binderClass = parameterizedAs(DataBinder.class, typeParametersOf(modelBean));
                  final String dataBinderVar = "dataBinder";
                  final String modelVar = "model";

                  createInstanceStmts.add(declareFinalVariable(dataBinderVar, binderClass,
                          invokeStatic(DataBinder.class, "forType", modelBean)));
                  createInstanceStmts
                  .add(declareFinalVariable(modelVar, modelBean, loadVariable(dataBinderVar).invoke("getModel")));
                  createInstanceStmts.add(loadVariable("this").invoke("setReference", loadVariable(modelVar),
                          DataBindingUtil.BINDER_VAR_NAME, loadVariable(dataBinderVar)));
                  createInstanceStmts.add(loadVariable(modelVar).returnValue());

                  return createInstanceStmts;
                }
              };

              provided = new DefaultCustomFactoryInjectable(handle.getType(), handle.getQualifier(),
                      nameGenerator.generateFor(handle.getType(), handle.getQualifier(),
                              InjectableType.ExtensionProvided),
                      Dependent.class, Collections.singletonList(WiringElementType.DependentBean), generator);
            }

            return provided;
          }
          else {
            throw new IllegalStateException("The " + DataBindingIOCExtension.class.getSimpleName()
                    + " was used to satisfy an injection site without the @" + Model.class.getName() + " annotation.");
          }
        }
      });
    }
  }
}
