package processing.mode.java.pdex;

import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.NumberLiteral;
import org.eclipse.jdt.core.dom.SimpleType;

import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import processing.mode.java.pdex.TextTransform.Edit;
import processing.mode.java.preproc.PdePreprocessor;

public class SourceUtils {


  public static final Pattern IMPORT_REGEX =
      Pattern.compile("(?:^|;)\\s*(import\\s+(?:(static)\\s+)?((?:\\w+\\s*\\.)*)\\s*(\\S+)\\s*;)",
                      Pattern.MULTILINE | Pattern.DOTALL);

  public static final Pattern IMPORT_REGEX_NO_KEYWORD =
      Pattern.compile("^\\s*((?:(static)\\s+)?((?:\\w+\\s*\\.)*)\\s*(\\S+))",
                      Pattern.MULTILINE | Pattern.DOTALL);

  public static List<ImportStatement> parseProgramImports(CharSequence source) {
    List<ImportStatement> result = new ArrayList<>();
    Matcher matcher = IMPORT_REGEX.matcher(source);
    while (matcher.find()) {
      ImportStatement is = ImportStatement.parse(matcher.toMatchResult());
      result.add(is);
    }
    return result;
  }

  public static List<Edit> parseProgramImports(CharSequence source,
                                               List<ImportStatement> outImports) {
    List<Edit> result = new ArrayList<>();
    Matcher matcher = IMPORT_REGEX.matcher(source);
    while (matcher.find()) {
      ImportStatement is = ImportStatement.parse(matcher.toMatchResult());
      outImports.add(is);
      int idx = matcher.start(1);
      int len = matcher.end(1) - idx;
      // Remove the import from the main program
      // Substitute with white spaces
      result.add(Edit.move(idx, len, 0));
      result.add(Edit.insert(0, "\n"));
    }
    return result;
  }



  // Positive lookahead and lookbehind are needed to match all type constructors
  // in code like `int(byte(245))` where first bracket matches as last
  // group in "^int(" but also as a first group in "(byte(". Lookahead and
  // lookbehind won't consume the shared character.
  public static final Pattern TYPE_CONSTRUCTOR_REGEX =
      Pattern.compile("(?<=^|\\W)(int|char|float|boolean|byte)(?=\\s*\\()",
                      Pattern.MULTILINE);

  public static List<Edit> replaceTypeConstructors(CharSequence source) {

    List<Edit> result = new ArrayList<>();

    Matcher matcher = TYPE_CONSTRUCTOR_REGEX.matcher(source);
    while (matcher.find()) {
      String match = matcher.group(1);
      int offset = matcher.start(1);
      int length = match.length();
      result.add(Edit.insert(offset, "PApplet."));
      String replace = "parse"
          + Character.toUpperCase(match.charAt(0)) + match.substring(1);
      result.add(Edit.replace(offset, length, replace));
    }

    return result;
  }



  public static final Pattern HEX_LITERAL_REGEX =
      Pattern.compile("(?<=^|\\W)(#[A-Fa-f0-9]{6})(?=\\W|$)");

  public static List<Edit> replaceHexLiterals(CharSequence source) {
    // Find all #[webcolor] and replace with 0xff[webcolor]
    // Should be 6 digits only.
    List<Edit> result = new ArrayList<>();

    Matcher matcher = HEX_LITERAL_REGEX.matcher(source);
    while (matcher.find()) {
      int offset = matcher.start(1);
      result.add(Edit.replace(offset, 1, "0xff"));
    }

    return result;
  }



  public static List<Edit> insertImports(List<ImportStatement> imports) {
    List<Edit> result = new ArrayList<>();
    for (ImportStatement imp : imports) {
      result.add(Edit.insert(0, imp.getFullSourceLine() + "\n"));
    }
    return result;
  }

  public static List<Edit> wrapSketch(PdePreprocessor.Mode mode, String className, int sourceLength) {

    List<Edit> edits = new ArrayList<>();

    StringBuilder b = new StringBuilder();

    // Header
    if (mode != PdePreprocessor.Mode.JAVA) {
      b.append("\npublic class ").append(className).append(" extends PApplet {\n");
      if (mode == PdePreprocessor.Mode.STATIC) {
        b.append("public void setup() {\n");
      }
    }

    edits.add(Edit.insert(0, b.toString()));

    // Reset builder
    b.setLength(0);

    // Footer
    if (mode != PdePreprocessor.Mode.JAVA) {
      if (mode == PdePreprocessor.Mode.STATIC) {
        // no noLoop() here so it does not tell you
        // "can't invoke noLoop() on obj" when you type "obj."
        b.append("\n}");
      }
      b.append("\n}\n");
    }

    edits.add(Edit.insert(sourceLength, b.toString()));

    return edits;
  }


  // Verifies that whole input String is floating point literal. Can't be used for searching.
  // https://docs.oracle.com/javase/specs/jls/se8/html/jls-3.html#jls-DecimalFloatingPointLiteral
  public static final Pattern FLOATING_POINT_LITERAL_VERIFIER;
  static {
    final String DIGITS = "(?:[0-9]|[0-9][0-9_]*[0-9])";
    final String EXPONENT_PART = "(?:[eE][+-]?" + DIGITS + ")";
    FLOATING_POINT_LITERAL_VERIFIER = Pattern.compile(
        "(?:^" + DIGITS + "\\." + DIGITS + "?" + EXPONENT_PART + "?[fFdD]?$)|" +
            "(?:^\\." + DIGITS + EXPONENT_PART + "?[fFdD]?$)|" +
            "(?:^" + DIGITS + EXPONENT_PART + "[fFdD]?$)|" +
            "(?:^" + DIGITS + EXPONENT_PART + "?[fFdD]$)");
  }

  // Mask to quickly resolve whether there are any access modifiers present
  private static final int ACCESS_MODIFIERS_MASK =
      Modifier.PUBLIC | Modifier.PRIVATE | Modifier.PROTECTED;

  public static List<Edit> preprocessAST(CompilationUnit cu) {
    final List<Edit> edits = new ArrayList<>();

    // Walk the tree
    cu.accept(new ASTVisitor() {
      @Override
      public boolean visit(SimpleType node) {
        // replace "color" with "int"
        if ("color".equals(node.getName().toString())) {
          edits.add(Edit.replace(node.getStartPosition(), node.getLength(), "int"));
        }
        return super.visit(node);
      }

      @Override
      public boolean visit(NumberLiteral node) {
        // add 'f' to floats
        String s = node.getToken().toLowerCase();
        if (FLOATING_POINT_LITERAL_VERIFIER.matcher(s).matches() && !s.endsWith("f") && !s.endsWith("d")) {
          edits.add(Edit.insert(node.getStartPosition() + node.getLength(), "f"));
        }
        return super.visit(node);
      }

      @Override
      public boolean visit(MethodDeclaration node) {
        // add 'public' to methods with default visibility
        int accessModifiers = node.getModifiers() & ACCESS_MODIFIERS_MASK;
        if (accessModifiers == 0) {
          edits.add(Edit.insert(node.getStartPosition(), "public "));
        }
        return super.visit(node);
      }
    });

    return edits;
  }


  public static final Pattern COLOR_TYPE_REGEX =
      Pattern.compile("(?:^|^\\p{javaJavaIdentifierPart})(color)\\s(?!\\s*\\()",
                      Pattern.MULTILINE | Pattern.UNICODE_CHARACTER_CLASS);

  public static List<Edit> replaceColorRegex(CharSequence source) {
    final List<Edit> edits = new ArrayList<>();

    Matcher matcher = COLOR_TYPE_REGEX.matcher(source);
    while (matcher.find()) {
      int offset = matcher.start(1);
      edits.add(Edit.replace(offset, 5, "int"));
    }

    return edits;
  }


  public static final Pattern NUMBER_LITERAL_REGEX =
      Pattern.compile("[-+]?[0-9]*\\.?[0-9]+(?:[eE][-+]?[0-9]+)?");

  public static List<Edit> fixFloatsRegex(CharSequence source) {
    final List<Edit> edits = new ArrayList<>();

    Matcher matcher = NUMBER_LITERAL_REGEX.matcher(source);
    while (matcher.find()) {
      int offset = matcher.start();
      int end = matcher.end();
      String group = matcher.group().toLowerCase();
      boolean isFloatingPoint = group.contains(".") || group.contains("e");
      boolean hasSuffix = end < source.length() &&
          Character.toLowerCase(source.charAt(end)) != 'f' &&
          Character.toLowerCase(source.charAt(end)) != 'd';
      if (isFloatingPoint && !hasSuffix) {
        edits.add(Edit.insert(offset, "f"));
      }
    }

    return edits;
  }


  static public String scrubCommentsAndStrings(String p) {
    StringBuilder sb = new StringBuilder(p);
    scrubCommentsAndStrings(sb);
    return sb.toString();
  }

  static public void scrubCommentsAndStrings(StringBuilder p) {

    final int length = p.length();

    final int OUT = 0;
    final int IN_BLOCK_COMMENT = 1;
    final int IN_EOL_COMMENT = 2;
    final int IN_STRING_LITERAL = 3;
    final int IN_CHAR_LITERAL = 4;

    int blockStart = -1;

    int prevState = OUT;
    int state = OUT;

    for (int i = 0; i <= length; i++) {
      char ch = (i < length) ? p.charAt(i) : 0;
      char pch = (i == 0) ? 0 : p.charAt(i-1);
      switch (state) {
        case OUT:
          switch (ch) {
            case '\'': state = IN_CHAR_LITERAL; break;
            case '"': state = IN_STRING_LITERAL; break;
            case '*': if (pch == '/') state = IN_BLOCK_COMMENT; break;
            case '/': if (pch == '/') state = IN_EOL_COMMENT; break;
          }
          break;
        case IN_BLOCK_COMMENT:
          if (pch == '*' && ch == '/' && (i - blockStart) > 1) {
            state = OUT;
          }
          break;
        case IN_EOL_COMMENT:
          if (ch == '\r' || ch == '\n') {
            state = OUT;
          }
          break;
        case IN_STRING_LITERAL:
          if ((pch != '\\' && ch == '"') || ch == '\r' || ch == '\n') {
            state = OUT;
          }
          break;
        case IN_CHAR_LITERAL:
          if ((pch != '\\' && ch == '\'') || ch == '\r' || ch == '\n') {
            state = OUT;
          }
          break;
      }

      // Terminate ongoing block at last char
      if (i == length) {
        state = OUT;
      }

      // Handle state changes
      if (state != prevState) {
        if (state != OUT) {
          // Entering block
          blockStart = i + 1;
        } else {
          // Exiting block
          int blockEnd = i;
          if (prevState == IN_BLOCK_COMMENT && i < length) blockEnd--; // preserve star in '*/'
          for (int j = blockStart; j < blockEnd; j++) {
            char c = p.charAt(j);
            if (c != '\n' && c != '\r') p.setCharAt(j, ' ');
          }
        }
      }

      prevState = state;
    }

  }

  static public List<JavaProblem> checkForMissingBraces(StringBuilder p, int[] tabStartOffsets) {
    List<JavaProblem> problems = new ArrayList<>(0);
    tabLoop: for (int tabIndex = 0; tabIndex < tabStartOffsets.length; tabIndex++) {
      int tabStartOffset = tabStartOffsets[tabIndex];
      int tabEndOffset = (tabIndex < tabStartOffsets.length - 1) ?
          tabStartOffsets[tabIndex + 1] : p.length();
      int depth = 0;
      int lineNumber = 0;
      for (int i = tabStartOffset; i < tabEndOffset; i++) {
        char ch = p.charAt(i);
        switch (ch) {
          case '{':
            depth++;
            break;
          case '}':
            depth--;
            break;
          case '\n':
            lineNumber++;
            break;
        }
        if (depth < 0) {
          JavaProblem problem =
              new JavaProblem("Found one too many } characters without { to match it.",
                              JavaProblem.ERROR, tabIndex, lineNumber);
          problem.setPDEOffsets(i - tabStartOffset, i - tabStartOffset + 1);
          problems.add(problem);
          continue tabLoop;
        }
      }
      if (depth > 0) {
        JavaProblem problem =
            new JavaProblem("Found one too many { characters without } to match it.",
                            JavaProblem.ERROR, tabIndex, lineNumber - 1);
        problem.setPDEOffsets(tabEndOffset - tabStartOffset - 2, tabEndOffset - tabStartOffset - 1);
        problems.add(problem);
      }
    }
    return problems;
  }

}
