/*
 * Copyright 2016-present Facebook, Inc.
 *
 * 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.intellij.ideabuck.autodeps;

import com.google.common.annotations.VisibleForTesting;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintWriter; // NOPMD not in core Buck
import java.io.UnsupportedEncodingException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class BuckDeps {
  private static final Logger LOG = Logger.getInstance(BuckDeps.class);

  // Patterns to search within a BUCK file for known rule
  private static final Pattern autodepsPattern = Pattern.compile("autodeps\\s*=\\s*True");
  private static final Pattern depsPattern = Pattern.compile("deps\\s*=\\s*\\[([^\\]]+)\\]");
  private static final Pattern exportedDepsPattern =
      Pattern.compile("exported_deps\\s*=\\s*\\[([^\\]]+)\\]");
  private static final Pattern visibilityPattern =
      Pattern.compile("visibility\\s*=\\s*\\[([^\\]]+)\\]");

  private BuckDeps() {}

  public static void addDeps(
      Project project,
      String importBuckPath,
      String currentBuckPath,
      String importTarget,
      String currentTarget) {
    String buckPath = project.getBaseDir().getPath() + "/" + currentBuckPath + "/BUCK";
    String contents = readBuckFile(buckPath);
    String dependency = "//" + importBuckPath + ":" + importTarget;
    String newContents = maybeAddDepToTarget(contents, dependency, currentTarget);
    if (!contents.equals(newContents)) {
      writeBuckFile(buckPath, newContents);
    }
  }

  public static void addVisibility(
      Project project,
      String importBuckPath,
      String currentBuckPath,
      String importTarget,
      String currentTarget) {
    String buckPath = project.getBaseDir().getPath() + "/" + importBuckPath + "/BUCK";
    String contents = readBuckFile(buckPath);
    String visibility = "//" + currentBuckPath + ":" + currentTarget;
    String newContents = maybeAddVisibilityToTarget(contents, visibility, importTarget);
    if (!contents.equals(newContents)) {
      writeBuckFile(buckPath, newContents);
    }
  }

  /**
   * Given the contents of a buckfile and a target name, returns an array of indices into the
   * content [start, end] of a guess where that target def in the given BUCK file.
   */
  @VisibleForTesting
  static int[] findTargetInBuckFileContents(String contents, String target) {
    int nameOffset = contents.indexOf("\"" + target + "\"");
    if (nameOffset == -1) {
      nameOffset = contents.indexOf("'" + target + "'");
    }
    if (nameOffset == -1) {
      return null; // target not found
    }
    // March backwards to figure out where the rule starts
    int targetStart = nameOffset;
    int depth = 1;
    while (depth > 0) {
      targetStart -= 1;
      if (targetStart <= 0) {
        return null; // marched back to beginning of file...this ain't it.
      }
      char ch = contents.charAt(targetStart);
      if (ch == '(') {
        depth -= 1;
      } else if (ch == ')') {
        depth += 1;
      }
    }
    // March forwards to figure out where the rule ends
    int targetEnd = nameOffset + target.length();
    depth = 1;
    while (depth > 0) {
      if (targetEnd >= contents.length()) {
        return null; // marched past end of file...didn't find it.
      }
      char ch = contents.charAt(targetEnd);
      if (ch == '(') {
        depth += 1;
      } else if (ch == ')') {
        depth -= 1;
      }
      targetEnd += 1;
    }
    return new int[] {targetStart, targetEnd};
  }

  /**
   * Given the contents of a BUCK file, try to modify the contents to add the given dependency to
   * the given target.
   */
  @VisibleForTesting
  static String maybeAddDepToTarget(String buckContents, String dependency, String target) {
    int[] targetOffset = findTargetInBuckFileContents(buckContents, target);
    if (targetOffset == null) {
      LOG.warn("Couldn't find target definition for " + target);
      // TODO(ideabuck):  make this a better parser
      return buckContents;
    }

    String targetDef = buckContents.substring(targetOffset[0], targetOffset[1]);

    if (autodepsPattern.matcher(targetDef).find()) {
      // TODO(ideabuck):  Once 'buck autodeps' stabilizes, should we invoke it here?
      return buckContents;
    }

    Matcher exportedDepsMatcher = exportedDepsPattern.matcher(targetDef);
    if (exportedDepsMatcher.find()) {
      String exportedDeps = exportedDepsMatcher.group(1);
      if (exportedDeps.contains(dependency)) {
        // If it already appears in the exported_deps section, nothing to do
        return buckContents;
      }
    }

    Matcher depsMatcher = depsPattern.matcher(targetDef);
    if (!depsMatcher.find()) {
      LOG.warn(
          "Couldn't figure out where to add new dependency on "
              + dependency
              + " in definition for "
              + target);
      return buckContents;
    }
    if (depsMatcher.group(1).contains(dependency)) {
      // already have dep in the deps section
      return buckContents;
    }
    int offset = targetOffset[0] + depsMatcher.start(1);
    return buckContents.substring(0, offset)
        + "\n\t\t\""
        + dependency
        + "\","
        + buckContents.substring(offset);
  }

  @VisibleForTesting
  static String maybeAddVisibilityToTarget(String buckContents, String visibility, String target) {

    int[] targetOffset = findTargetInBuckFileContents(buckContents, target);
    if (targetOffset == null) {
      LOG.warn("Couldn't find target definition for " + target);
      // TODO(ideabuck):  make this a better parser
      return buckContents;
    }

    String targetDef = buckContents.substring(targetOffset[0], targetOffset[1]);

    Matcher visibilityMatcher = visibilityPattern.matcher(targetDef);
    if (!visibilityMatcher.find()) {
      LOG.warn(
          "Couldn't figure out where to increase visibility for "
              + target
              + " to include "
              + visibility);
      // TODO(ideabuck):  try harder to figure out where to add the visibility
      return buckContents;
    }

    if (visibilityMatcher.group(1).contains(visibility)) {
      return buckContents; // already visibile to this caller
    }
    if (visibilityMatcher.group(1).contains("PUBLIC")) {
      return buckContents; // already visible everywhere
    }
    int offset = targetOffset[0] + visibilityMatcher.start(1);
    buckContents =
        buckContents.substring(0, offset)
            + "\n\t\t\""
            + visibility
            + "\","
            + buckContents.substring(offset);
    return buckContents;
  }

  private static String readBuckFile(String buckFilePath) {
    String str = "";
    File file = new File(buckFilePath);
    try (FileInputStream fis = new FileInputStream(file)) {

      byte[] data = new byte[(int) file.length()];
      fis.read(data);
      fis.close();
      str = new String(data, "UTF-8");
    } catch (FileNotFoundException e) {
      LOG.error(e.toString());
    } catch (UnsupportedEncodingException e) {
      LOG.error(e.toString());
    } catch (IOException e) {
      LOG.error(e.toString());
    }
    return str;
  }

  private static void writeBuckFile(String buckFilePath, String text) {

    try (PrintWriter out = new PrintWriter(buckFilePath)) { // NOPMD not in core Buck
      out.write(text);
    } catch (FileNotFoundException e) {
      LOG.error(e.toString());
    } catch (IOException e) {
      LOG.error(e.toString());
    }
  }
}
