/*
 * Copyright (c) Facebook, Inc. and 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 com.facebook.buck.cxx.toolchain.impl;

import com.facebook.buck.core.config.BuckConfig;
import com.facebook.buck.core.exceptions.HumanReadableException;
import com.facebook.buck.core.model.Flavor;
import com.facebook.buck.core.model.FlavorDomain;
import com.facebook.buck.core.model.InternalFlavor;
import com.facebook.buck.core.model.TargetConfiguration;
import com.facebook.buck.core.toolchain.ToolchainCreationContext;
import com.facebook.buck.core.toolchain.ToolchainFactory;
import com.facebook.buck.core.toolchain.ToolchainInstantiationException;
import com.facebook.buck.core.toolchain.ToolchainProvider;
import com.facebook.buck.core.util.log.Logger;
import com.facebook.buck.cxx.config.CxxBuckConfig;
import com.facebook.buck.cxx.toolchain.CxxPlatform;
import com.facebook.buck.cxx.toolchain.CxxPlatformsProvider;
import com.facebook.buck.cxx.toolchain.CxxPlatformsSupplier;
import com.facebook.buck.cxx.toolchain.ProviderBackedCxxPlatform;
import com.facebook.buck.cxx.toolchain.UnresolvedCxxPlatform;
import com.facebook.buck.util.environment.Platform;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;

public class CxxPlatformsProviderFactory implements ToolchainFactory<CxxPlatformsProvider> {

  private static final Logger LOG = Logger.get(CxxPlatformsProviderFactory.class);

  @Override
  public Optional<CxxPlatformsProvider> createToolchain(
      ToolchainProvider toolchainProvider,
      ToolchainCreationContext context,
      TargetConfiguration toolchainTargetConfiguration) {
    Iterable<String> toolchainNames =
        toolchainProvider.getToolchainsWithCapability(CxxPlatformsSupplier.class);

    ImmutableMap.Builder<Flavor, UnresolvedCxxPlatform> cxxSystemPlatforms = ImmutableMap.builder();
    for (String toolchainName : toolchainNames) {
      if (toolchainProvider.isToolchainPresent(toolchainName, toolchainTargetConfiguration)) {
        CxxPlatformsSupplier cxxPlatformsSupplier =
            toolchainProvider.getByName(
                toolchainName, toolchainTargetConfiguration, CxxPlatformsSupplier.class);
        cxxSystemPlatforms.putAll(cxxPlatformsSupplier.getUnresolvedCxxPlatforms());
      }
    }

    try {
      return Optional.of(createProvider(context.getBuckConfig(), cxxSystemPlatforms.build()));
    } catch (HumanReadableException e) {
      throw ToolchainInstantiationException.wrap(e);
    }
  }

  private static CxxPlatformsProvider createProvider(
      BuckConfig config, ImmutableMap<Flavor, UnresolvedCxxPlatform> cxxSystemPlatforms) {
    Platform platform = Platform.detect();
    CxxBuckConfig cxxBuckConfig = new CxxBuckConfig(config);

    // Create a map of system platforms.
    ImmutableMap.Builder<Flavor, UnresolvedCxxPlatform> cxxSystemPlatformsBuilder =
        ImmutableMap.builder();

    cxxSystemPlatformsBuilder.putAll(cxxSystemPlatforms);

    CxxPlatform defaultHostCxxPlatform = DefaultCxxPlatforms.build(platform, cxxBuckConfig);
    cxxSystemPlatformsBuilder.put(
        defaultHostCxxPlatform.getFlavor(),
        new StaticUnresolvedCxxPlatform(defaultHostCxxPlatform));
    ImmutableMap<Flavor, UnresolvedCxxPlatform> cxxSystemPlatformsMap =
        cxxSystemPlatformsBuilder.build();

    cxxSystemPlatformsMap =
        appendHostPlatformIfNeeded(defaultHostCxxPlatform, cxxSystemPlatformsMap);

    Map<Flavor, UnresolvedCxxPlatform> cxxOverridePlatformsMap =
        updateCxxPlatformsWithOptionsFromBuckConfig(
            platform, config, cxxSystemPlatformsMap, defaultHostCxxPlatform);

    UnresolvedCxxPlatform hostCxxPlatform =
        getHostCxxPlatform(cxxBuckConfig, cxxOverridePlatformsMap);
    cxxOverridePlatformsMap.put(hostCxxPlatform.getFlavor(), hostCxxPlatform);

    ImmutableMap<Flavor, UnresolvedCxxPlatform> cxxPlatformsMap =
        ImmutableMap.<Flavor, UnresolvedCxxPlatform>builder()
            .putAll(cxxOverridePlatformsMap)
            .build();

    // Build up the final list of C/C++ platforms.
    FlavorDomain<UnresolvedCxxPlatform> cxxPlatforms =
        new FlavorDomain<>("C/C++ platform", cxxPlatformsMap);

    // Get the default target platform from config.
    UnresolvedCxxPlatform defaultCxxPlatform =
        CxxPlatforms.getConfigDefaultCxxPlatform(cxxBuckConfig, cxxPlatformsMap, hostCxxPlatform);

    return CxxPlatformsProvider.of(defaultCxxPlatform, cxxPlatforms);
  }

  private static ImmutableMap<Flavor, UnresolvedCxxPlatform> appendHostPlatformIfNeeded(
      CxxPlatform defaultHostCxxPlatform,
      ImmutableMap<Flavor, UnresolvedCxxPlatform> cxxSystemPlatforms) {
    Flavor hostFlavor = CxxPlatforms.getHostFlavor();
    if (!cxxSystemPlatforms.containsKey(hostFlavor)) {
      return ImmutableMap.<Flavor, UnresolvedCxxPlatform>builder()
          .putAll(cxxSystemPlatforms)
          .put(
              hostFlavor,
              new StaticUnresolvedCxxPlatform(
                  CxxPlatform.builder().from(defaultHostCxxPlatform).setFlavor(hostFlavor).build()))
          .build();
    } else {
      return cxxSystemPlatforms;
    }
  }

  /**
   * Add platforms for each cxx flavor obtained from the buck config files from sections of the form
   * cxx#{flavor name}. These platforms are overrides for existing system platforms.
   */
  private static Map<Flavor, UnresolvedCxxPlatform> updateCxxPlatformsWithOptionsFromBuckConfig(
      Platform platform,
      BuckConfig config,
      ImmutableMap<Flavor, UnresolvedCxxPlatform> cxxSystemPlatformsMap,
      CxxPlatform defaultHostCxxPlatform) {
    ImmutableSet<Flavor> possibleHostFlavors = CxxPlatforms.getAllPossibleHostFlavors();
    Map<Flavor, UnresolvedCxxPlatform> cxxOverridePlatformsMap =
        new HashMap<>(cxxSystemPlatformsMap);
    ImmutableSet<Flavor> cxxFlavors = CxxBuckConfig.getCxxFlavors(config);
    for (Flavor flavor : cxxFlavors) {
      Optional<UnresolvedCxxPlatform> newPlatform =
          CxxBuckConfig.getProviderBasedPlatform(config, flavor);

      if (!newPlatform.isPresent()) {
        newPlatform =
            augmentSystemPlatform(
                platform,
                cxxSystemPlatformsMap,
                defaultHostCxxPlatform,
                possibleHostFlavors,
                flavor,
                new CxxBuckConfig(config, flavor));
      }

      if (!newPlatform.isPresent()) {
        continue;
      }

      cxxOverridePlatformsMap.put(flavor, newPlatform.get());
    }
    return cxxOverridePlatformsMap;
  }

  private static Optional<UnresolvedCxxPlatform> augmentSystemPlatform(
      Platform platform,
      ImmutableMap<Flavor, UnresolvedCxxPlatform> cxxSystemPlatformsMap,
      CxxPlatform defaultHostCxxPlatform,
      ImmutableSet<Flavor> possibleHostFlavors,
      Flavor flavor,
      CxxBuckConfig cxxConfig) {
    UnresolvedCxxPlatform baseUnresolvedCxxPlatform = cxxSystemPlatformsMap.get(flavor);
    if (baseUnresolvedCxxPlatform instanceof ProviderBackedCxxPlatform) {
      return Optional.of(baseUnresolvedCxxPlatform);
    }
    CxxPlatform baseCxxPlatform;
    if (baseUnresolvedCxxPlatform == null) {
      if (possibleHostFlavors.contains(flavor)) {
        // If a flavor is for an alternate host, it's safe to skip.
        return Optional.empty();
      }
      LOG.info("Applying \"%s\" overrides to default host platform", flavor);
      baseCxxPlatform = defaultHostCxxPlatform;
    } else {
      if (!(baseUnresolvedCxxPlatform instanceof StaticUnresolvedCxxPlatform)) {
        throw new HumanReadableException("Cannot override non-static cxx platform %s", flavor);
      }
      baseCxxPlatform = ((StaticUnresolvedCxxPlatform) baseUnresolvedCxxPlatform).getCxxPlatform();
    }

    StaticUnresolvedCxxPlatform augmentedPlatform =
        new StaticUnresolvedCxxPlatform(
            CxxPlatforms.copyPlatformWithFlavorAndConfig(
                baseCxxPlatform, platform, cxxConfig, flavor));
    return Optional.of(augmentedPlatform);
  }

  private static UnresolvedCxxPlatform getHostCxxPlatform(
      CxxBuckConfig cxxBuckConfig, Map<Flavor, UnresolvedCxxPlatform> cxxOverridePlatformsMap) {
    Flavor hostFlavor = CxxPlatforms.getHostFlavor();

    if (!cxxBuckConfig.getShouldRemapHostPlatform()) {
      hostFlavor = DefaultCxxPlatforms.FLAVOR;
    }
    Optional<String> hostCxxPlatformOverride = cxxBuckConfig.getHostPlatform();
    if (hostCxxPlatformOverride.isPresent()) {
      Flavor overrideFlavor = InternalFlavor.of(hostCxxPlatformOverride.get());
      if (cxxOverridePlatformsMap.containsKey(overrideFlavor)) {
        hostFlavor = overrideFlavor;
      }
    }
    UnresolvedCxxPlatform hostFlavoredPlatform =
        Objects.requireNonNull(cxxOverridePlatformsMap.get(hostFlavor));
    UnresolvedCxxPlatform hostCxxPlatform;
    if (!cxxBuckConfig.getShouldRemapHostPlatform()) {
      hostCxxPlatform = hostFlavoredPlatform.withFlavor(DefaultCxxPlatforms.FLAVOR);
    } else {
      hostCxxPlatform = hostFlavoredPlatform;
    }
    return hostCxxPlatform;
  }
}
