/*
 * Copyright (C) 2016 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 java.util.Arrays.stream;
import static java.util.stream.Collectors.toCollection;

import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import java.util.stream.Stream;

import org.jboss.errai.codegen.meta.MetaClass;
import org.jboss.errai.ioc.client.api.IOCExtension;
import org.jboss.errai.ioc.rebind.ioc.bootstrapper.IOCProcessingContext;
import org.jboss.errai.ioc.rebind.ioc.bootstrapper.IOCProcessor;
import org.jboss.errai.ioc.rebind.ioc.extension.IOCExtensionConfigurator;
import org.jboss.errai.ioc.rebind.ioc.injector.api.ExtensionTypeCallback;
import org.jboss.errai.ioc.rebind.ioc.injector.api.InjectionContext;

import com.google.common.collect.HashMultimap;
import com.google.common.collect.HashMultiset;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multiset;

import jsinterop.annotations.JsType;

/**
 * This extension tracks non-native {@link JsType} interfaces that are implemented by 0 or 1 classes. Why? Because in
 * order for use of {@link JsType} instances between scripts to work, the compiler must not prune or inline method calls
 * to a {@link JsType} interface.
 *
 * If the compiler finds only a single implementation of an interface, it will replace all method calls on the interface
 * with method calls on the implementing class.
 *
 * If the compiler finds no implementations of an interface, it will generate method calls on null!
 *
 * By generating dummy implementations, we ensure the the compiler is never able to "optimize" calls on {@link JsType}
 * interfaces in these ways.
 *
 * @author Max Barkley <mbarkley@redhat.com>
 */
@IOCExtension
public class JsTypeAntiInliningExtension implements IOCExtensionConfigurator {

  private static Multiset<MetaClass> requiringDummyImpls = null;

  public static boolean requiresAntiInliningDummy(final MetaClass type) {
    return requiringDummyImpls.contains(type);
  }

  public static int numberOfRequiredAntiInliningDummies(final MetaClass type) {
    return requiringDummyImpls.count(type);
  }

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

  @Override
  public void afterInitialization(final IOCProcessingContext context, final InjectionContext injectionContext) {
    if (IOCProcessor.isJsInteropSupportEnabled()) {
      injectionContext.registerExtensionTypeCallback(new ExtensionTypeCallback() {
        final Multimap<MetaClass, MetaClass> jsTypeIfaceImpls = HashMultimap.create();
        final Set<MetaClass> jsTypeIfaces = new HashSet<>();
        @Override
        public void init() {
          requiringDummyImpls = null;
        }

        @Override
        public void callback(final MetaClass type) {
          if (!type.getFullyQualifiedName().startsWith("java.util")) {
            if (type.isInterface()) {
              jsTypeIfaces.add(type);
            }
            else if (type.isConcrete() && type.isPublic()) {
              findJsTypeIfaces(type)
              .forEach(iface -> {
                jsTypeIfaces.add(iface);
                jsTypeIfaceImpls.put(iface, type);
              });
            }
          }
        }

        @Override
        public void finish() {
          final Multiset<MetaClass> noOrSingleImplJsTypeIfaces =
                  jsTypeIfaces
                  .stream()
                  .flatMap(iface -> {
                    final Collection<MetaClass> impls = jsTypeIfaceImpls.get(iface);
                    if (impls.isEmpty()) {
                      return stream(new MetaClass[] { iface, iface });
                    }
                    else if (impls.size() == 1) {
                      return stream(new MetaClass[] { iface });
                    }
                    else {
                      return stream(new MetaClass[0]);
                    }
                  })
                  .collect(toCollection(() -> HashMultiset.create()));
          requiringDummyImpls = noOrSingleImplJsTypeIfaces;
        }
      });
    }
  }

  private Stream<MetaClass> findJsTypeIfaces(final MetaClass type) {
    return stream(type.getInterfaces())
      .flatMap(iface -> stream(iface.getInterfaces()))
      .distinct()
      .filter(iface -> !iface.getFullyQualifiedName().startsWith("java.util"))
      .filter(iface -> iface.isAnnotationPresent(JsType.class) && !iface.getAnnotation(JsType.class).isNative());
  }

}
