/*
 * Copyright 2011 Google Inc. 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.j2objc.gen;

import com.google.common.base.CharMatcher;
import com.google.common.io.LineReader;
import com.google.devtools.j2objc.ast.TreeNode;
import com.google.devtools.j2objc.util.UnicodeUtils;
import java.io.IOException;
import java.io.StringReader;

/**
 * Builds source text.  This is similar to a StringBuilder, but tracks line
 * numbers and outputs them as CPP line directives when directed.
 *
 * @author Tom Ball
 */
public class SourceBuilder {
  private final StringBuilder buffer = new StringBuilder();
  private String currentFile;
  private int indention = 0;
  private int currentLine = -1;

  /**
   * If true, generate CPP line directives.  It's necessary to store this
   * here rather than directly use Options.getLineDirectives(), so that the
   * header generator doesn't generate them.
   */
  private final boolean emitLineDirectives;

  public static final int DEFAULT_INDENTION = 2;
  public static final int BEGINNING_OF_FILE = -1;

  /**
   * Create a new SourceBuilder.
   *
   * @param emitLineDirectives if true, generate CPP line directives
   */
  public SourceBuilder(boolean emitLineDirectives) {
    this(emitLineDirectives, BEGINNING_OF_FILE);
  }

  /**
   * Create a new SourceBuilder, specifying the initial line number to begin
   * syncing.  This is normally only used for building complex statements,
   * where the generated source is used in another builder.
   *
   * @param emitLineDirectives if true, generate CPP line directives
   * @param startLine the initial line number, or -1 if at start of file
   */
  public SourceBuilder(boolean emitLineDirectives, int startLine) {
    this.emitLineDirectives = emitLineDirectives;
    this.currentLine = startLine;
  }

  @Override
  public String toString() {
    return buffer.toString();
  }

  private static final CharMatcher NEWLINE_MATCHER = CharMatcher.is('\n');

  public void print(String s) {
    buffer.append(s);
    currentLine += NEWLINE_MATCHER.countIn(s);
  }

  public void print(char c) {
    buffer.append(c);
    if (c == '\n') {
      currentLine++;
    }
  }

  public void print(char[] cs) {
    for (char c : cs) {
      print(c);
    }
  }

  public void print(int i) {
    buffer.append(i);
  }

  public void printf(String format, Object... args) {
    print(UnicodeUtils.format(format, args));
  }

  public void println(String s) {
    print(s);
    newline();
  }

  public void println(char c) {
    print(c);
    newline();
  }

  public void newline() {
    buffer.append('\n');
    currentLine++;
  }

  public void indent() {
    indention++;
  }

  public void unindent() {
    indention--;
    if (indention < 0) {
      throw new AssertionError("unbalanced indents");
    }
  }

  public void printIndent() {
    buffer.append(pad(indention * DEFAULT_INDENTION));
  }

  // StringBuilder compatibility.
  public SourceBuilder append(char c) {
    print(c);
    return this;
  }

  public SourceBuilder append(char[] c) {
    print(c);
    return this;
  }

  public SourceBuilder append(int i) {
    print(i);
    return this;
  }

  public SourceBuilder append(String s) {
    print(s);
    return this;
  }

  public char charAt(int i) {
    return buffer.charAt(i);
  }

  public int length() {
    return buffer.length();
  }

  public String substring(int start, int end) {
    return buffer.substring(start, end);
  }

  public void replace(int start, int end, String str) {
    buffer.replace(start, end, str);
  }

  public char[] pad(int n) {
    if (n < 0) {
      n = 0;
    }
    char[] result = new char[n];
    for (int i = 0; i < n; i++) {
      result[i] = ' ';
    }
    return result;
  }

  public void reset() {
    buffer.setLength(0);
  }

  public void syncLineNumbers(TreeNode node) {
    if (emitLineDirectives) {
      int sourceLine = node.getLineNumber();
      if (sourceLine > 0 && currentLine != sourceLine) {
        buffer.append(UnicodeUtils.format("\n#line %d\n", sourceLine));
        currentLine = sourceLine;
      }
    }
  }

  /**
   * Emits a #line directive setting the given filename.
   * @param fileName the filename to sync
   */
  public void syncFilename(String fileName) {
    if (emitLineDirectives) {
      if (!fileName.equals(currentFile)) {
        currentLine = BEGINNING_OF_FILE;
        // C11 spec. (6.10.4) requires a line number between 1 and 2147483647.
        buffer.append(UnicodeUtils.format("\n#line 1 \"%s\"\n", fileName));
      }
    }
    currentFile = fileName;
  }

  /**
   * Fix line indention, based on brace count.
   */
  public String reindent(String code) {
    try {
      // Remove indention from each line.
      StringBuilder sb = new StringBuilder();
      LineReader lr = new LineReader(new StringReader(code));
      String line = lr.readLine();
      while (line != null) {
        sb.append(line.trim());
        line = lr.readLine();
        if (line != null) {
          sb.append('\n');
        }
      }
      String strippedCode = sb.toString();

      // Now indent it again.
      int indent = indention * DEFAULT_INDENTION;
      sb.setLength(0);  // reset buffer
      lr = new LineReader(new StringReader(strippedCode));
      line = lr.readLine();
      while (line != null) {
        if (line.startsWith("}")) {
          indent -= DEFAULT_INDENTION;
        }
        if (!line.startsWith("#line")) {
          sb.append(pad(indent));
        }
        sb.append(line);
        if (line.endsWith("{")) {
          indent += DEFAULT_INDENTION;
        }
        line = lr.readLine();
        if (line != null) {
          sb.append('\n');
        }
      }
      return sb.toString();
    } catch (IOException e) {
      // Should never happen with string readers.
      throw new AssertionError(e);
    }
  }

  public int getCurrentLine() {
    return currentLine;
  }
}
