// Copyright 2014 The Bazel Authors. All rights reserved.
//
// 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.google.devtools.build.lib.rules.cpp;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.devtools.build.lib.vfs.Path;
import java.io.ByteArrayOutputStream;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * A Class for filtering the output of /showIncludes from MSVC compiler.
 *
 * <p>A discovered header file will be printed with prefix "Note: including file:", the path is
 * collected, and the line is suppressed from the actual output users can see.
 *
 * <p>Also suppress the basename of source file, which is printed unconditionally by MSVC compiler,
 * there is no way to turn it off.
 */
public class ShowIncludesFilter {

  private FilterShowIncludesOutputStream filterShowIncludesOutputStream;
  private final String sourceFileName;

  public ShowIncludesFilter(String sourceFileName) {
    this.sourceFileName = sourceFileName;
  }

  /**
   * Use this class to filter and collect the headers discovered by MSVC compiler, also filter out
   * the source file name printed unconditionally by the compiler.
   */
  public static class FilterShowIncludesOutputStream extends FilterOutputStream {

    private final ByteArrayOutputStream buffer = new ByteArrayOutputStream(4096);
    private final Collection<String> dependencies = new ArrayList<>();
    private static final int NEWLINE = '\n';
    // "Note: including file:" in 14 languages,
    // cl.exe will print different prefix according to the locale configured for MSVC.
    private static final ImmutableList<String> SHOW_INCLUDES_PREFIXES =
        ImmutableList.of(
            new String(
                new byte[] {
                  78, 111, 116, 101, 58, 32, 105, 110, 99, 108, 117, 100, 105, 110, 103, 32, 102,
                  105, 108, 101, 58
                },
                StandardCharsets.UTF_8), // English
            new String(
                new byte[] {
                  -26, -77, -88, -26, -124, -113, 58, 32, -27, -116, -123, -27, -112, -85, -26, -86,
                  -108, -26, -95, -120, 58
                },
                StandardCharsets.UTF_8), // Traditional Chinese
            new String(
                new byte[] {
                  80, 111, 122, 110, -61, -95, 109, 107, 97, 58, 32, 86, -60, -115, 101, 116, 110,
                  -60, -101, 32, 115, 111, 117, 98, 111, 114, 117, 58
                },
                StandardCharsets.UTF_8), // Czech
            new String(
                new byte[] {
                  72, 105, 110, 119, 101, 105, 115, 58, 32, 69, 105, 110, 108, 101, 115, 101, 110,
                  32, 100, 101, 114, 32, 68, 97, 116, 101, 105, 58
                },
                StandardCharsets.UTF_8), // German
            new String(
                new byte[] {
                  82, 101, 109, 97, 114, 113, 117, 101, -62, -96, 58, 32, 105, 110, 99, 108, 117,
                  115, 105, 111, 110, 32, 100, 117, 32, 102, 105, 99, 104, 105, 101, 114, -62, -96,
                  58
                },
                StandardCharsets.UTF_8), // French
            new String(
                new byte[] {
                  78, 111, 116, 97, 58, 32, 102, 105, 108, 101, 32, 105, 110, 99, 108, 117, 115, 111
                },
                StandardCharsets.UTF_8), // Italian
            new String(
                new byte[] {
                  -29, -125, -95, -29, -125, -94, 58, 32, -29, -126, -92, -29, -125, -77, -29, -126,
                  -81, -29, -125, -85, -29, -125, -68, -29, -125, -119, 32, -29, -125, -107, -29,
                  -126, -95, -29, -126, -92, -29, -125, -85, 58
                },
                StandardCharsets.UTF_8), // Janpanese
            new String(
                new byte[] {
                  -20, -80, -72, -22, -77, -96, 58, 32, -19, -113, -84, -19, -107, -88, 32, -19,
                  -116, -116, -20, -99, -68, 58
                },
                StandardCharsets.UTF_8), // Korean
            new String(
                new byte[] {
                  85, 119, 97, 103, 97, 58, 32, 119, 32, 116, 121, 109, 32, 112, 108, 105, 107, 117,
                  58
                },
                StandardCharsets.UTF_8), // Polish
            new String(
                new byte[] {
                  79, 98, 115, 101, 114, 118, 97, -61, -89, -61, -93, 111, 58, 32, 105, 110, 99,
                  108, 117, 105, 110, 100, 111, 32, 97, 114, 113, 117, 105, 118, 111, 58
                },
                StandardCharsets.UTF_8), // Portuguese
            new String(
                new byte[] {
                  -48, -97, -47, -128, -48, -72, -48, -68, -48, -75, -47, -121, -48, -80, -48, -67,
                  -48, -72, -48, -75, 58, 32, -48, -78, -48, -70, -48, -69, -47, -114, -47, -121,
                  -48, -75, -48, -67, -48, -72, -48, -75, 32, -47, -124, -48, -80, -48, -71, -48,
                  -69, -48, -80, 58
                },
                StandardCharsets.UTF_8), // Russian
            new String(
                new byte[] {
                  78, 111, 116, 58, 32, 101, 107, 108, 101, 110, 101, 110, 32, 100, 111, 115, 121,
                  97, 58
                },
                StandardCharsets.UTF_8), // Turkish
            new String(
                new byte[] {
                  -26, -77, -88, -26, -124, -113, 58, 32, -27, -116, -123, -27, -112, -85, -26,
                  -106, -121, -28, -69, -74, 58
                },
                StandardCharsets.UTF_8), // Simplified Chinese
            new String(
                new byte[] {
                  78, 111, 116, 97, 58, 32, 105, 110, 99, 108, 117, 115, 105, -61, -77, 110, 32,
                  100, 101, 108, 32, 97, 114, 99, 104, 105, 118, 111, 58
                },
                StandardCharsets.UTF_8) // Spanish
            );
    private final String sourceFileName;
    private boolean sawPotentialUnsupportedShowIncludesLine;
    // Grab everything under the execroot base so that external repository header files are covered
    // in the sibling repository layout.
    private static final Pattern EXECROOT_BASE_HEADER_PATTERN =
        Pattern.compile(".*execroot\\\\(?<headerPath>.*)");
    // Match a line of the form "fooo: bar:   C:\some\path\file.h". As this is relatively generic,
    // we require the line to include an absolute path with drive letter. If remote workers rewrite
    // the path to a relative one, we won't match it, but it is unlikely that such setups use an
    // unsupported encoding. We also exclude any matches that contain numbers: MSVC warnings and
    // errors always contain numbers, but the /showIncludes output doesn't in any encoding since all
    // codepages are ASCII-compatible.
    private static final Pattern POTENTIAL_UNSUPPORTED_SHOW_INCLUDES_LINE =
        Pattern.compile("[^:0-9]+:\\s+[^:0-9]+:\\s+[A-Za-z]:\\\\[^:]*\\\\execroot\\\\[^:]*");

    public FilterShowIncludesOutputStream(OutputStream out, String sourceFileName) {
      super(out);
      this.sourceFileName = sourceFileName;
    }

    @Override
    public void write(int b) throws IOException {
      buffer.write(b);
      if (b == NEWLINE) {
        String line = buffer.toString(StandardCharsets.UTF_8.name());
        boolean prefixMatched = false;
        for (String prefix : SHOW_INCLUDES_PREFIXES) {
          if (line.startsWith(prefix)) {
            line = line.substring(prefix.length()).trim();
            Matcher m = EXECROOT_BASE_HEADER_PATTERN.matcher(line);
            if (m.matches()) {
              // Prefix the matched header path with "..\". This way, external repo header paths are
              // resolved to "<execroot>\..\<repo name>\<path>", and main repo file paths are
              // resolved to "<execroot>\..\<main repo>\<path>", which is nicely normalized to
              // "<execroot>\<path>".
              line = "..\\" + m.group("headerPath");
            }
            dependencies.add(line);
            prefixMatched = true;
            break;
          }
        }
        // cl.exe also prints out the file name unconditionally, we need to also filter it out.
        if (!prefixMatched && !line.trim().equals(sourceFileName)) {
          // When the toolchain definition failed to force an English locale, /showIncludes lines
          // can use non-UTF8 encodings, which the checks above fail to detect. As this results in
          // incorrect incremental builds, we emit a warning if the raw byte sequence comprising the
          // line looks like it could be a /showIncludes line.
          if (POTENTIAL_UNSUPPORTED_SHOW_INCLUDES_LINE
              .matcher(buffer.toString(StandardCharsets.ISO_8859_1).trim())
              .matches()) {
            sawPotentialUnsupportedShowIncludesLine = true;
          }
          buffer.writeTo(out);
        }
        buffer.reset();
      }
    }

    @Override
    public void flush() throws IOException {
      String line = buffer.toString(StandardCharsets.UTF_8.name());

      // If this line starts or could start with a prefix.
      boolean startingWithAnyPrefix = false;
      for (String prefix : SHOW_INCLUDES_PREFIXES) {
        if (line.startsWith(prefix) || prefix.startsWith(line)) {
          startingWithAnyPrefix = true;
          break;
        }
      }

      if (!startingWithAnyPrefix
          // If this line starts or could start with the source file name.
          && !line.startsWith(sourceFileName)
          && !sourceFileName.startsWith(line)) {
        buffer.writeTo(out);
        buffer.reset();
      }
      out.flush();
    }

    public Collection<String> getDependencies() {
      return this.dependencies;
    }

    public boolean sawPotentialUnsupportedShowIncludesLine() {
      return sawPotentialUnsupportedShowIncludesLine;
    }
  }

  public FilterOutputStream getFilteredOutputStream(OutputStream outputStream) {
    filterShowIncludesOutputStream =
        new FilterShowIncludesOutputStream(outputStream, sourceFileName);
    return filterShowIncludesOutputStream;
  }

  public Collection<Path> getDependencies(Path root) {
    Collection<Path> dependenciesInPath = new ArrayList<>();
    if (filterShowIncludesOutputStream != null) {
      for (String dep : filterShowIncludesOutputStream.getDependencies()) {
        dependenciesInPath.add(root.getRelative(dep));
      }
    }
    return Collections.unmodifiableCollection(dependenciesInPath);
  }

  public boolean sawPotentialUnsupportedShowIncludesLine() {
    return filterShowIncludesOutputStream != null
        && filterShowIncludesOutputStream.sawPotentialUnsupportedShowIncludesLine();
  }

  @VisibleForTesting
  Collection<String> getDependencies() {
    return filterShowIncludesOutputStream.getDependencies();
  }
}
