// 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.docgen;

import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.LinkedListMultimap;
import com.google.common.collect.ListMultimap;
import com.google.devtools.build.lib.analysis.ConfiguredRuleClassProvider;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import javax.annotation.Nullable;

/**
 * A helper class to read and process documentations for rule classes and attributes
 * from exactly one java source file.
 */
public class SourceFileReader {

  private Collection<RuleDocumentation> ruleDocEntries;
  private ListMultimap<String, RuleDocumentationAttribute> attributeDocEntries;
  private final ConfiguredRuleClassProvider ruleClassProvider;
  private final String javaSourceFilePath;
  private final String sourceUrl;

  public SourceFileReader(
      ConfiguredRuleClassProvider ruleClassProvider, String javaSourceFilePath, String sourceUrl) {
    this.ruleClassProvider = ruleClassProvider;
    this.javaSourceFilePath = javaSourceFilePath;
    this.sourceUrl = sourceUrl;
  }

  /**
   * The handler class of the line read from the text file.
   */
  public abstract static class ReadAction {

    // Text file line indexing starts from 1
    private int lineCnt = 1;

    protected abstract void readLineImpl(String line)
        throws BuildEncyclopediaDocException, IOException;

    protected int getLineCnt() {
      return lineCnt;
    }

    public void readLine(String line)
        throws BuildEncyclopediaDocException, IOException {
      readLineImpl(line);
      lineCnt++;
    }
  }

  private static final String LS = DocgenConsts.LS;

  /**
   * Reads the attribute and rule documentation present in the file represented by
   * SourceFileReader.javaSourceFilePath. The rule doc variables are added to the rule
   * documentation (which therefore must be defined in the same file). The attribute docs are
   * stored in a different class member, so they need to be handled outside this method.
   */
  public void readDocsFromComments() throws BuildEncyclopediaDocException, IOException {
    final Map<String, RuleDocumentation> docMap = new HashMap<>();
    final List<RuleDocumentationVariable> docVariables = new LinkedList<>();
    final ListMultimap<String, RuleDocumentationAttribute> docAttributes =
        LinkedListMultimap.create();
    readTextFile(
        javaSourceFilePath,
        new ReadAction() {

          private boolean inBlazeRuleDocs = false;
          private boolean inBlazeRuleVarDocs = false;
          private boolean inBlazeAttributeDocs = false;
          private boolean inFamilySummary = false;
          private StringBuilder sb = new StringBuilder();
          private String ruleName;
          private String familySummary = "";
          private String ruleType;
          private String ruleFamily;
          private String variableName;
          private String attributeName;
          private ImmutableSet<String> flags;
          private int startLineCnt;

          @Override
          public void readLineImpl(String line) throws BuildEncyclopediaDocException {
            // TODO(bazel-team): check if copy paste code can be reduced using inner classes
            if (inBlazeRuleDocs) {
              if (DocgenConsts.BLAZE_RULE_END.matcher(line).matches()) {
                endBlazeRuleDoc(docMap);
              } else {
                appendLine(line);
              }
            } else if (inBlazeRuleVarDocs) {
              if (DocgenConsts.BLAZE_RULE_VAR_END.matcher(line).matches()) {
                endBlazeRuleVarDoc(docVariables);
              } else {
                appendLine(line);
              }
            } else if (inBlazeAttributeDocs) {
              if (DocgenConsts.BLAZE_RULE_ATTR_END.matcher(line).matches()) {
                endBlazeAttributeDoc(docAttributes);
              } else {
                appendLine(line);
              }
            } else if (inFamilySummary) {
              if (DocgenConsts.FAMILY_SUMMARY_END.matcher(line).matches()) {
                endFamilySummary();
              } else {
                appendLine(line);
              }
            }
            Matcher familySummaryStartMatcher = DocgenConsts.FAMILY_SUMMARY_START.matcher(line);
            Matcher ruleStartMatcher = DocgenConsts.BLAZE_RULE_START.matcher(line);
            Matcher ruleVarStartMatcher = DocgenConsts.BLAZE_RULE_VAR_START.matcher(line);
            Matcher ruleAttrStartMatcher = DocgenConsts.BLAZE_RULE_ATTR_START.matcher(line);
            if (familySummaryStartMatcher.find()) {
              startFamilySummary();
            } else if (ruleStartMatcher.find()) {
              startBlazeRuleDoc(line, ruleStartMatcher);
            } else if (ruleVarStartMatcher.find()) {
              startBlazeRuleVarDoc(ruleVarStartMatcher);
            } else if (ruleAttrStartMatcher.find()) {
              startBlazeAttributeDoc(line, ruleAttrStartMatcher);
            }
          }

          private void appendLine(String line) {
            // Add another line of html code to the building rule documentation
            // Removing whitespace and java comment asterisk from the beginning of the line
            sb.append(line.replaceAll("^[\\s]*\\*", "") + LS);
          }

          private void startBlazeRuleDoc(String line, Matcher matcher)
              throws BuildEncyclopediaDocException {
            sb = new StringBuilder();
            checkDocValidity();
            // Start of a new rule.
            // e.g.: matcher.group(1) = "NAME = cc_binary, TYPE = BINARY, FAMILY = C / C++"
            for (String group : Splitter.on(",").split(matcher.group(1))) {
              List<String> parts = Splitter.on("=").limit(2).splitToList(group);
              boolean good = false;
              if (parts.size() == 2) {
                String key = parts.get(0).trim();
                String value = parts.get(1).trim();
                good = true;
                if (DocgenConsts.META_KEY_NAME.equals(key)) {
                  ruleName = value;
                } else if (DocgenConsts.META_KEY_TYPE.equals(key)) {
                  ruleType = value;
                } else if (DocgenConsts.META_KEY_FAMILY.equals(key)) {
                  ruleFamily = value;
                } else {
                  good = false;
                }
              }
              if (!good) {
                System.err.printf(
                    "WARNING: bad rule definition in line %d: '%s'", getLineCnt(), line);
              }
            }

            startLineCnt = getLineCnt();
            addFlags(line);
            inBlazeRuleDocs = true;
          }

          private void startFamilySummary() {
            sb = new StringBuilder();
            inFamilySummary = true;
          }

          private void endFamilySummary() {
            familySummary = sb.toString();
          }

          private void endBlazeRuleDoc(final Map<String, RuleDocumentation> documentations)
              throws BuildEncyclopediaDocException {
            // End of a rule, create RuleDocumentation object
            documentations.put(
                ruleName,
                new RuleDocumentation(
                    ruleName,
                    ruleType,
                    ruleFamily,
                    sb.toString(),
                    javaSourceFilePath,
                    getLineCnt(),
                    sourceUrl,
                    flags,
                    familySummary));
            sb = new StringBuilder();
            inBlazeRuleDocs = false;
          }

          private void startBlazeRuleVarDoc(Matcher matcher) throws BuildEncyclopediaDocException {
            checkDocValidity();
            // Start of a new rule variable
            ruleName = matcher.group(1).replaceAll("[\\s]", "");
            variableName = matcher.group(2).replaceAll("[\\s]", "");
            startLineCnt = getLineCnt();
            inBlazeRuleVarDocs = true;
          }

          private void endBlazeRuleVarDoc(final List<RuleDocumentationVariable> docVariables) {
            // End of a rule, create RuleDocumentationVariable object
            docVariables.add(
                new RuleDocumentationVariable(ruleName, variableName, sb.toString(), startLineCnt));
            sb = new StringBuilder();
            inBlazeRuleVarDocs = false;
          }

          private void startBlazeAttributeDoc(String line, Matcher matcher)
              throws BuildEncyclopediaDocException {
            checkDocValidity();
            // Start of a new attribute
            ruleName = matcher.group(1).replaceAll("[\\s]", "");
            attributeName = matcher.group(2).replaceAll("[\\s]", "");
            startLineCnt = getLineCnt();
            addFlags(line);
            inBlazeAttributeDocs = true;
          }

          private void endBlazeAttributeDoc(
              final ListMultimap<String, RuleDocumentationAttribute> docAttributes) {
            // End of a attribute, create RuleDocumentationAttribute object
            docAttributes.put(
                attributeName,
                RuleDocumentationAttribute.create(
                    ruleClassProvider.getRuleClassDefinition(ruleName).getClass(),
                    attributeName,
                    sb.toString(),
                    javaSourceFilePath,
                    startLineCnt,
                    flags));
            sb = new StringBuilder();
            inBlazeAttributeDocs = false;
          }

          private void addFlags(String line) {
            // Add flags if there's any
            Matcher matcher = DocgenConsts.BLAZE_RULE_FLAGS.matcher(line);
            if (matcher.find()) {
              flags = ImmutableSet.<String>copyOf(matcher.group(1).split(","));
            } else {
              flags = ImmutableSet.<String>of();
            }
          }

          private void checkDocValidity() throws BuildEncyclopediaDocException {
            if (inBlazeRuleDocs || inBlazeRuleVarDocs || inBlazeAttributeDocs) {
              throw new BuildEncyclopediaDocException(
                  javaSourceFilePath,
                  getLineCnt(),
                  "Malformed documentation, #BLAZE_RULE started after another #BLAZE_RULE.");
            }
          }
        });

    // Adding rule doc variables to the corresponding rules
    for (RuleDocumentationVariable docVariable : docVariables) {
      if (docMap.containsKey(docVariable.getRuleName())) {
        docMap.get(docVariable.getRuleName()).addDocVariable(
          docVariable.getVariableName(), docVariable.getValue());
      } else {
        throw new BuildEncyclopediaDocException(javaSourceFilePath, docVariable.getStartLineCnt(),
            String.format("Malformed rule variable #BLAZE_RULE(%s).%s, rule %s not found in file.",
                docVariable.getRuleName(), docVariable.getVariableName(),
                docVariable.getRuleName()));
      }
    }
    ruleDocEntries = docMap.values();
    attributeDocEntries = docAttributes;
  }

  public Collection<RuleDocumentation> getRuleDocEntries() {
    return ruleDocEntries;
  }

  public ListMultimap<String, RuleDocumentationAttribute> getAttributeDocEntries() {
    return attributeDocEntries;
  }

  /**
   * Reads the template file without variable substitution.
   */
  public static String readTemplateContents(String templateFilePath)
      throws BuildEncyclopediaDocException, IOException {
    return readTemplateContents(templateFilePath, null);
  }

  /**
   * Reads a template file and substitutes variables of the format ${FOO}.
   *
   * @param variables keys are the possible variable names, e.g. "FOO", values are the substitutions
   *     (can be null)
   */
  public static String readTemplateContents(String templateFilePath,
      final Map<String, String> variables) throws BuildEncyclopediaDocException, IOException {
    final StringBuilder sb = new StringBuilder();
    readTextFile(templateFilePath, new ReadAction() {
      @Override
      public void readLineImpl(String line) {
        sb.append(expandVariables(line, variables)).append(LS);
      }
    });
    return sb.toString();
  }

  private static String expandVariables(String line, Map<String, String> variables) {
    if (variables == null || line.indexOf("${") == -1) {
      return line;
    }

    for (Map.Entry<String, String> variable : variables.entrySet()) {
      line = line.replace("${" + variable.getKey() + "}", variable.getValue());
    }
    return line;
  }

  @Nullable
  private static BufferedReader createReader(String filePath) throws IOException {
    File file = new File(filePath);
    if (file.exists()) {
      return Files.newBufferedReader(file.toPath(), StandardCharsets.UTF_8);
    } else {
      InputStream is = SourceFileReader.class.getResourceAsStream(filePath);
      if (is != null) {
        return new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8));
      } else {
        return null;
      }
    }
  }

  public static void readTextFile(String filePath, ReadAction action)
      throws BuildEncyclopediaDocException, IOException {
    try (BufferedReader br = createReader(filePath)) {
      if (br != null) {
        String line = null;
        while ((line = br.readLine()) != null) {
          action.readLine(line);
        }
      } else {
        System.out.println("Couldn't find file or resource: " + filePath);
      }
    }
  }
}
