/*
 * RiskScape™ Copyright New Zealand Institute for Earth Science Limited
 * (Earth Sciences New Zealand) is distributed for research purposes only
 * under the terms of AGPLv3.
 *
 * RiskScape™ Copyright 2026 New Zealand Institute for Earth Science
 * Limited (Earth Sciences New Zealand). All rights reserved. Source code
 * available under the AGPLv3.
 * 
 * This program is free software: you can redistribute it and/or modify it under
 *  the terms of the GNU Affero General Public License as published by the Free
 *  Software Foundation, either version 3 of the License, or (at your option) any
 *  later version.
 * 
 * This program is distributed for RESEARCH PURPOSES ONLY, in the hope that it will
 * be useful for research and education initiatives.
 * 
 * If you are not a researcher, or you are a researcher who wishes to use this
 * program on terms other than AGPLv3 (including those who wish to restrict the
 * distribution of any source code created using this program), please contact:
 * https://riskscape.org.nz
 * 
 * This program is distributed WITHOUT ANY WARRANTY; without even the implied
 * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Affero General Public License for more details.  You should have received a copy
 * of the GNU Affero General Public License along with this program.  If not, see
 * <http://www.gnu.org/licenses/>.
 * 
 * By way of summary only, under the AGPLv3:
 *     • Permissions of this strongest copyleft license are conditioned
 *       on making available complete source code of licensed works and
 *       modifications, which include larger works using a licensed work,
 *       under the same license.
 *     • Copyright and license notices must be preserved.
 *     • Contributors provide an express grant of patent rights.
 *     • When a modified version is used to provide a service over a
 *       network, the complete source code of the modified version must be made
 *       available.
 */
package nz.org.riskscape.ini;

import nz.org.riskscape.dsl.LexerException;
import nz.org.riskscape.dsl.LexerProblems;
import nz.org.riskscape.dsl.LexingMatcher;
import nz.org.riskscape.dsl.LexingStream;
import nz.org.riskscape.dsl.SourceLocation;

class TokenMatchers {
  private static final char[] QUOTE_CHARS = "'\"`".toCharArray();
  private static final char[] SPACE_OR_TAB = " \t".toCharArray();

  public static final LexingMatcher VALUE = (tokenType, stream) -> {
    SourceLocation startsAt = stream.nextIf("=");
    if (startsAt == null) {
      // values must start with '='
      return null;
    }
    stream.skipWhile(SPACE_OR_TAB);

    StringBuilder builder = new StringBuilder();
    boolean eatWhiteSpace = false;

    while (!stream.isEof()) {
      if (eatWhiteSpace) {
        stream.skipWhile(SPACE_OR_TAB);
        eatWhiteSpace = false;
        continue;
      }
      if (skipIfLineEnding(stream)) {
        // values are terminated by line endings
        break;
      }
      char lastChar = stream.getLastChar();
      char ch = stream.next();
      if (isTrailingComment(ch, lastChar)) {
        // values are terminated by a comment, but we need to comsume the comment
        // from the stream.
        while (!stream.isEof() && !skipIfLineEnding(stream)) {
          stream.next();
        }
        break;
      }
      if (skipIfEscapedLineEnding(ch, stream)) {
        // <backslash><whitespace><linefeed> is a line continuation. don't add these chars to the buffer
        // but turn on eatWhiteSpace
        eatWhiteSpace = true;
        continue;
      }

      builder.append(ch);
    }

    return stream.newToken(tokenType, startsAt, builder.toString());
  };

  /**
   * Returns true if a line ending is next in the source. If there is a line ending then it is also
   * consumed.
   *
   * Line endings could be:
   * - \n
   * - \r
   * - \r\n
   */
  private static boolean skipIfLineEnding(LexingStream stream) {
    char c = stream.peek();
    if (c == '\r' || c == '\n') {
      //yup it's a line ending, swallow it
      stream.next();
      if (c == '\r' && stream.peek() == '\n') {
        stream.next();
      }
      return true;
    }
    return false;
  }

  /**
   * Returns true if the remainder of the current line is a backslash,
   * followed by any number of spaces or tabs - which indicates a line
   * continuation
   */
  private static boolean skipIfEscapedLineEnding(char ch, LexingStream stream) {

    if (ch != '\\') {
      return false;
    }

    SourceLocation start = stream.getLocation();
    stream.skipWhile(SPACE_OR_TAB);
    if (!skipIfLineEnding(stream)) {
      stream.rewind(start);
      return false;
    }
    return true;
  }

  private static boolean isTrailingComment(char currentChar, char lastChar) {
    if (currentChar == '#' || currentChar == ';') {
      return lastChar == ' ' || lastChar == '\t';
    }
    return false;
  }

  public static final LexingMatcher MULTILINE_VALUE = (tokenType, stream) -> {
    SourceLocation startsAt = stream.nextIf("=");
    if (startsAt == null) {
      // values must start with '='
      return null;
    }

    stream.skipWhile(SPACE_OR_TAB);

    char quoteChar = stream.nextIfOneOf(QUOTE_CHARS);

    // not a quote, give up
    if (quoteChar == '\0') {
      return null;
    }

    // we need another two.
    if (!stream.nextIf(quoteChar)) {
      return null;
    }
    if (!stream.nextIf(quoteChar)) {
      return null;
    }

    // we are now consuming a multiline value, set up some state
    int quoteCount = 0;
    StringBuilder buffer = new StringBuilder();

    // skip any leading spaces or tabs
    stream.skipWhile(SPACE_OR_TAB);

    // throw away the first line if it was empty
    eatNewLine(stream);

    while (!stream.isEof()) {
      char ch = stream.next();

      if (ch == quoteChar) {
        quoteCount++;

        if (quoteCount == 3) {
          // we are done!
          return stream.newToken(tokenType, startsAt, buffer.toString());
        }
      } else if (quoteCount > 0) {
        // append the quotes that we counted
        while (quoteCount > 0) {
          buffer.append(quoteChar);
          quoteCount--;
        }
        buffer.append(ch);
      } else {
        buffer.append(ch);
      }
    }

    throw new LexerException(LexerProblems.get().eofInString(stream.getIndex()));
  };

  private static void eatNewLine(LexingStream stream) {
    char ch = stream.peek();

    // this will consume \r, \n or \r\n
    if (ch == '\r') {
      stream.next();
    }
    if (ch == '\n') {
      stream.next();
    }
  }
}
