// Copyright (c) 2010 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.debug.jsdtbridge;

import org.chromium.debug.core.model.JavaScriptFormatter;
import org.chromium.debug.core.model.StringMappingData;
import org.eclipse.text.edits.ReplaceEdit;
import org.eclipse.text.edits.TextEdit;
import org.eclipse.wst.jsdt.core.JavaScriptCore;
import org.eclipse.wst.jsdt.core.ToolFactory;
import org.eclipse.wst.jsdt.core.formatter.CodeFormatter;

/**
 * JSDT-based implementation of {@link JavaScriptFormatter}.
 */
public class JsdtFormatterBridge implements JavaScriptFormatter {
  public Result format(String sourceString) {
    TextEdit textEdit = jsdtFormat(sourceString);

    if (textEdit == null) {
      final boolean useFallbackFormatter = true;
      if (useFallbackFormatter) {
        // While JSDT formatter has chances to fail
        // (see https://bugs.eclipse.org/bugs/show_bug.cgi?id=329716),
        // there is a fall-back implementation, that only insert new-lines in some places
        // thus making a source a bit more readable.
        String header = Messages.JsdtFormatterBridge_FALLBACK_COMMENT;
        textEdit = AdHocFormatter.format(sourceString, header);
      } else {
        throw new RuntimeException("Formatter failed"); //$NON-NLS-1$
      }
    }
    return convertResult(sourceString, textEdit);
  }

  private TextEdit jsdtFormat(String sourceString) {
    CodeFormatter jsdtFormatter = ToolFactory.createCodeFormatter(
        JavaScriptCore.getDefaultOptions());

    TextEdit textEdit = jsdtFormatter.format(CodeFormatter.K_JAVASCRIPT_UNIT,
        sourceString, 0, sourceString.length(), 0, LINE_END_STRING);
    return textEdit;
  }

  private Result convertResult(String sourceString, TextEdit textEdit) {
    IntBuffer intBuffer = new IntBuffer();

    final Position origPos = new Position(0, 0);
    final Position dstPos = new Position(0, 0);

    TextEdit[] editList = textEdit.getChildren();

    int sourceStringPos = 0;
    int editListPos = 0;

    int nextLineEndPos = sourceString.indexOf(LINE_END_CHAR);

    final StringBuilder builder = new StringBuilder();

    // Iterate over all edits and all untouched line ends.
    while (true) {
      ReplaceEdit replaceEdit;
      int nextEditPos = -1;
      { // Find next applicable edit. This is a potential cycle if we skip some changes.
        if (editListPos < editList.length) {
          if (editList[editListPos] instanceof ReplaceEdit == false) {
            throw new RuntimeException();
          }
          replaceEdit = (ReplaceEdit) editList[editListPos];
          nextEditPos = replaceEdit.getOffset();
        } else {
          replaceEdit = null;
        }
      }

      // Choose what comes first: line end or edit.
      boolean processLineEndNotEdit;
      if (nextEditPos == -1) {
        if (nextLineEndPos == -1) {
          break;
        } else {
          processLineEndNotEdit = true;
        }
      } else {
        if (nextLineEndPos == -1) {
          processLineEndNotEdit = false;
        } else {
          processLineEndNotEdit = nextLineEndPos < nextEditPos;
        }
      }
      if (processLineEndNotEdit) {
        // Process next line end.
        builder.append(sourceString.substring(sourceStringPos, nextLineEndPos + 1));
        origPos.line++;
        origPos.col = 0;
        dstPos.line++;
        dstPos.col = 0;
        sourceStringPos = nextLineEndPos + 1;
        nextLineEndPos = sourceString.indexOf(LINE_END_CHAR, sourceStringPos);
      } else {
        // Process next edit.
        builder.append(sourceString.substring(sourceStringPos, nextEditPos));
        origPos.col += nextEditPos - sourceStringPos;
        dstPos.col += nextEditPos - sourceStringPos;

        origPos.writeToArray(intBuffer);
        dstPos.writeToArray(intBuffer);

        // Count removed line ends.
        if (replaceEdit.getLength() > 0) {
          String removedString = sourceString.substring(replaceEdit.getOffset(),
              replaceEdit.getOffset() + replaceEdit.getLength());
          origPos.advanceToString(removedString);
        }
        // Count added line ends.
        builder.append(replaceEdit.getText());
        dstPos.advanceToString(replaceEdit.getText());

        origPos.writeToArray(intBuffer);
        dstPos.writeToArray(intBuffer);

        sourceStringPos = nextEditPos + replaceEdit.getLength();
        editListPos++;

        if (nextLineEndPos != -1 && nextLineEndPos < sourceStringPos) {
          nextLineEndPos = sourceString.indexOf(LINE_END_CHAR, sourceStringPos);
        }
      }
    }
    builder.append(sourceString.substring(sourceStringPos));
    origPos.col += sourceString.length() - sourceStringPos;
    dstPos.col += sourceString.length() - sourceStringPos;

    final int[] inputArray = new int[intBuffer.size() / 2];
    final int[] formattedArray = new int[intBuffer.size() / 2];

    for (int i = 0; i < inputArray.length; i += 2) {
      inputArray[i] = intBuffer.get(i * 2 + 0);
      inputArray[i + 1] = intBuffer.get(i * 2 + 1);
      formattedArray[i] = intBuffer.get(i * 2 + 2);
      formattedArray[i + 1] = intBuffer.get(i * 2 + 3);
    }

    final StringMappingData inputTextData =
        new StringMappingData(inputArray, origPos.line, origPos.col);

    final StringMappingData formattedTextData =
        new StringMappingData(formattedArray, dstPos.line, dstPos.col);


    return new Result() {
      public String getFormattedText() {
        return builder.toString();
      }

      public StringMappingData getInputTextData() {
        return inputTextData;
      }

      public StringMappingData getFormattedTextData() {
        return formattedTextData;
      }
    };
  }

  private static class Position {
    Position(int line, int col) {
      this.line = line;
      this.col = col;
    }
    int line;
    int col;

    void advanceToString(String str) {
      int innerPos = 0;
      while (true) {
        int innerLineEndPos = str.indexOf('\n', innerPos);
        if (innerLineEndPos == -1) {
          break;
        }
        line++;
        col = 0;
        innerPos = innerLineEndPos + 1;
      }
      col += str.length() - innerPos;
    }

    void writeToArray(IntBuffer buffer) {
      buffer.add2(line, col);
    }
  }

  // Exponentially-growing buffer for ints.
  private static class IntBuffer {
    private int[] array = new int[INITIAL_SIZE];
    private int pos = 0;

    public void add2(int i1, int i2) {
      if (pos + 2 > array.length) {
        int[] newArray = new int[array.length * 2];
        System.arraycopy(array, 0, newArray, 0, pos);
        array = newArray;
      }
      array[pos] = i1;
      array[pos + 1] = i2;
      pos += 2;
    }

    public int get(int pos) {
      return array[pos];
    }

    public int size() {
      return pos;
    }

    private static int INITIAL_SIZE = 10;
  }

  private static final char LINE_END_CHAR = '\n';
  private static final String LINE_END_STRING = LINE_END_CHAR + ""; //$NON-NLS-1$
}
