/*******************************************************************************
 * Copyright (c) 2022 Martin Weber.
 *
 * Content is provided to you under the terms and conditions of the Eclipse Public License Version 2.0 "EPL".
 * A copy of the EPL is available at http://www.eclipse.org/legal/epl-2.0.
 *
 * SPDX-License-Identifier: EPL-2.0
 *******************************************************************************/

package de.marw.cmake4eclipse.mbs.internal;

import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;

/**
 * Parses the error output stream of cmake and reports errors and warnings as problem markers.
 * <p>
 * As long as cmake keeps printing the output we are interested in to the standard error stream, attaching an instance
 * of this class to cmake's standard <b>output</b> stream will have no positive effect.
 * </p>
 * <p>
 * NOTE: There is no way properly handle output emitted by the cmake {@code MESSAGE(NOTICE "text")} or
 * {@code MESSAGE("text")} command since that output is arbitrary text w/o any indication of the message type nor
 * filename/line-number information.
 * </p>
 *
 * @author Martin Weber
 */
/* package */ class CMakeErrorParser implements AutoCloseable {

  /** matches the Start of a message, also ending the previous message */
  private static final Pattern PTN_MSG_START;

  private static Map<String, MessageHandler> handlersByMessageStart = new HashMap<>();

  /** the handler for the message we are currently gathering output for */
  private MessageHandler currentHandler = new MhNull();
  private final ICMakeExecutionMarkerFactory markerFactory;
  private final StringBuilder buffer;

  static {
    // setup regex to match the start of a message...
    StringBuilder ptnbuf = new StringBuilder("^"); //$NON-NLS-1$

    List<MessageHandler> markerHandlers = Arrays.asList(new MhStartLog(), new MhStatus(), new MhDeprError(),
        new MhDeprWarning(), new MhErrorDev(), new MhError(), new MhInternalError(), new MhWarningDev(),
        new MhWarning());
    for (Iterator<MessageHandler> it = markerHandlers.iterator(); it.hasNext();) {
      MessageHandler h = it.next();
      handlersByMessageStart.put(h.getMessageStart(), h);
      ptnbuf.append(Pattern.quote(h.getMessageStart()));
      if (it.hasNext()) {
        ptnbuf.append('|');
      }
    }

    PTN_MSG_START = Pattern.compile(ptnbuf.toString());
  }

  ////////////////////////////////////////////////////////////////////
  /**
   * @param markerFactory the object responsible for creating problem marker objects for the project being built
   */
  public CMakeErrorParser(ICMakeExecutionMarkerFactory markerFactory) {
    this.markerFactory = Objects.requireNonNull(markerFactory);
    buffer = new StringBuilder(512);
  }

  /**
   * Adds text from the output stream to parse.
   *
   * @param input text from the output stream to parse
   */
  public void addInput(CharSequence input) {
    buffer.append(input);
    processBuffer();
  }

  /**
   * Closes this parser. Any remaining buffered input will be parsed.
   */
  @Override
  public void close() {
    // process remaining bytes
    processMessage(currentHandler, buffer.toString().trim());
    buffer.delete(0, buffer.length());
  }

  /**
   * Deletes all CMake error markers on the specified project.
   *
   * @param project
   *          the project where to remove the error markers.
   * @throws CoreException
   */
  public static void deleteErrorMarkers(IProject project) throws CoreException {
    project.deleteMarkers(ICMakeExecutionMarkerFactory.CMAKE_PROBLEM_MARKER_ID, false, IResource.DEPTH_INFINITE);
  }

  private void processBuffer() {
    Matcher matcher = PTN_MSG_START.matcher(""); //$NON-NLS-1$
    while (true) {
      matcher.reset(buffer.subSequence(currentHandler.getMessageStart().length(), buffer.length()));
      if (matcher.find()) {
        int end = matcher.start() + currentHandler.getMessageStart().length();
        String message = buffer.substring(0, end);
        processMessage(currentHandler, message.trim());
        String handlerId = matcher.group();
        MessageHandler newHandler = handlersByMessageStart.get(handlerId);
        currentHandler = newHandler;
        buffer.delete(0, end); // delete processed message
        continue; // proceed with follow-up messages
      } else {
        // NO message arrived in buffer
        return; // wait for more input
      }
    }
  }

  /**
   * @param handler     message handler
   * @param fullMessage the complete message, including the string the message starts with
   */
  private void processMessage(MessageHandler handler, String fullMessage) {
    try {
      handler.processMessage(markerFactory, fullMessage);
    } catch (CoreException e) {
      Activator.getDefault().getLog()
          .log(new Status(IStatus.WARNING, Activator.PLUGIN_ID, "CMake output error parsing failed", e)); //$NON-NLS-1$
    }
  }

  ////////////////////////////////////////////////////////////////////
  // inner classes
  ////////////////////////////////////////////////////////////////////
  /**
   * Message handler base class. Extracts the source-file name and line-number of errors from the output stream.
   * <p>
   * Message matching regexes are taken from cmake code in cmMessenger.cxx#printMessagePreamble: <code>
   *
   * <pre>
  if (t == cmake::FATAL_ERROR) {
  msg << "CMake Error";
  } else if (t == cmake::INTERNAL_ERROR) {
  msg << "CMake Internal Error (please report a bug)";
  } else if (t == cmake::LOG) {
  msg << "CMake Debug Log";
  } else if (t == cmake::DEPRECATION_ERROR) {
  msg << "CMake Deprecation Error";
  } else if (t == cmake::DEPRECATION_WARNING) {
  msg << "CMake Deprecation Warning";
  } else if (t == cmake::AUTHOR_WARNING) {
  msg << "CMake Warning (dev)";
  } else if (t == cmake::AUTHOR_ERROR) {
  msg << "CMake Error (dev)";
  } else {
  msg << "CMake Warning";
   * </pre>
   *
   * <code><br>
   * NOTE: We cannot properly handle output emitted by the cmake MESSAGE(NOTICE ...) command since the output is
   * arbitrary text w/o any indication of the message type nor filename/line-number information.
   * </p>
   *
   * @author Martin Weber
   */
  private static abstract class MessageHandler {

    /** patterns used to extract file-name and line number information */
    private static final Pattern[] PTN_LOCATION;

    /** Name of the named-capturing group that holds a file name. */
    private static final String GRP_FILE = "File"; //$NON-NLS-1$
    /** Name of the named-capturing group that holds a line number. */
    private static final String GRP_LINE = "Lineno"; //$NON-NLS-1$

    static {
      PTN_LOCATION = new Pattern[] { Pattern.compile("(?m)^ at (?<" + GRP_FILE + ">.+):(?<" + GRP_LINE + ">\\d+).*$"), //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
          Pattern.compile("(?s)^: Error in cmake code at.(?<" + GRP_FILE + ">.+):(?<" + GRP_LINE + ">\\d+).*$"), //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
          Pattern.compile("(?m)^ in (?<" + GRP_FILE + ">.+):(?<" + GRP_LINE + ">\\d+).*$"), //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
          Pattern.compile("(?m)^ in (?<" + GRP_FILE + ">.+?):.*$"), //$NON-NLS-1$ //$NON-NLS-2$
          Pattern.compile("(?m)^:\\s.+$"), }; //$NON-NLS-1$
    }

    /**
     * Gets the string the error message is supposed to start with.
     */
    abstract String getMessageStart();

    /**
     * @return the severity of the problem, see {@link IMarker} for acceptable severity values
     */
    abstract int getSeverity();

    /**
     * Creates the {@link IMarker marker object} that reflects the message.
     *
     * @param markerFactory the object responsible for creating problem marker objects for the project being built
     * @param fullMessage   the complete message, including the string the message starts with
     * @throws CoreException
     */
    public void processMessage(ICMakeExecutionMarkerFactory markerFactory, String fullMessage) throws CoreException {
      int length = getMessageStart().length();
      if(fullMessage.length() <= length) {
        return;
      }
      String content = fullMessage.substring(length);
      // mandatory attributes for the marker
      Map<String, Object> attributes = new HashMap<>(3);
      attributes.put(IMarker.LOCATION, getClass().getSimpleName());

      // filename is normally project source root relative but may be absolute FS path
      String filename = null;
      for (Pattern ptn : PTN_LOCATION) {
        final Matcher matcher = ptn.matcher(content);
        // try to extract filename and/or line number from message
        if (matcher.find()) {
          try {
            filename = matcher.group(GRP_FILE);
          } catch (IllegalArgumentException expected) {
            // no file name in message
          }
          // attach additional info to marker...
          try {
            String lineno = matcher.group(GRP_LINE);
            Integer lineNumber = Integer.parseInt(lineno);
            attributes.put(IMarker.LINE_NUMBER, lineNumber);
          } catch (IllegalArgumentException expected) {
            // no line number in message
          }
          break;
        }
      }
      markerFactory.createMarker(fullMessage, getSeverity(), filename, attributes);
    }
  } // MessageHandler

  ////////////////////////////////////////////////////////////////////
  private static class MhNull extends MessageHandler {

    @Override
    public void processMessage(ICMakeExecutionMarkerFactory markerFactory, String fullMessage) throws CoreException {
    }

    @Override
    String getMessageStart() {
      return ""; //$NON-NLS-1$
    }

    @Override
    int getSeverity() {
      throw new UnsupportedOperationException();
    }

    @Override
    public String toString() {
      return super.toString() + ": " + getMessageStart(); //$NON-NLS-1$
    }
  }

  ////////////////////////////////////////////////////////////////////
  private static class MhStartLog extends MhNull {
    private static final String START_LOG = "CMake Debug Log"; //$NON-NLS-1$

    @Override
    String getMessageStart() {
      return START_LOG;
    }

    @Override
    int getSeverity() {
      throw new UnsupportedOperationException();
    }

    @Override
    public String toString() {
      return super.toString() + ": " + getMessageStart(); //$NON-NLS-1$
    }
  }

  ////////////////////////////////////////////////////////////////////
  private static class MhStatus extends MhNull {
    private static final String START_STATUS = "-- "; //$NON-NLS-1$

    @Override
    String getMessageStart() {
      return START_STATUS;
    }

    @Override
    int getSeverity() {
      throw new UnsupportedOperationException();
    }

    @Override
    public String toString() {
      return super.toString() + ": " + getMessageStart(); //$NON-NLS-1$
    }
  }

  ////////////////////////////////////////////////////////////////////
  private static class MhDeprError extends MessageHandler {
    private static final String START_DERROR = "CMake Deprecation Error"; //$NON-NLS-1$

    @Override
    String getMessageStart() {
      return START_DERROR;
    }

    @Override
    int getSeverity() {
      return IMarker.SEVERITY_ERROR;
    }

    @Override
    public String toString() {
      return super.toString() + ": " + getMessageStart(); //$NON-NLS-1$
    }
  }

  private static class MhDeprWarning extends MessageHandler {
    private static final String START_DWARNING = "CMake Deprecation Warning"; //$NON-NLS-1$

    @Override
    String getMessageStart() {
      return START_DWARNING;
    }

    @Override
    int getSeverity() {
      return IMarker.SEVERITY_WARNING;
    }

    @Override
    public String toString() {
      return super.toString() + ": " + getMessageStart(); //$NON-NLS-1$
    }
  }

  private static class MhError extends MessageHandler {
    private static final String START_ERROR = "CMake Error"; //$NON-NLS-1$

    @Override
    String getMessageStart() {
      return START_ERROR;
    }

    @Override
    int getSeverity() {
      return IMarker.SEVERITY_ERROR;
    }

    @Override
    public String toString() {
      return super.toString() + ": " + getMessageStart(); //$NON-NLS-1$
    }
  }

  private static class MhErrorDev extends MessageHandler {
    private static final String START_ERROR_DEV = "CMake Error (dev)"; //$NON-NLS-1$

    @Override
    String getMessageStart() {
      return START_ERROR_DEV;
    }

    @Override
    int getSeverity() {
      return IMarker.SEVERITY_ERROR;
    }

    @Override
    public String toString() {
      return super.toString() + ": " + getMessageStart(); //$NON-NLS-1$
    }
  }

  private static class MhInternalError extends MessageHandler {
    private static final String START_IERROR = "CMake Internal Error (please report a bug)"; //$NON-NLS-1$

    @Override
    String getMessageStart() {
      return START_IERROR;
    }

    @Override
    int getSeverity() {
      return IMarker.SEVERITY_ERROR;
    }

    @Override
    public String toString() {
      return super.toString() + ": " + getMessageStart(); //$NON-NLS-1$
    }
  }

  private static class MhWarning extends MessageHandler {
    private static final String START_WARNING = "CMake Warning"; //$NON-NLS-1$

    @Override
    String getMessageStart() {
      return START_WARNING;
    }

    @Override
    int getSeverity() {
      return IMarker.SEVERITY_WARNING;
    }

    @Override
    public String toString() {
      return super.toString() + ": " + getMessageStart(); //$NON-NLS-1$
    }
  }

  private static class MhWarningDev extends MessageHandler {
    private static final String START_WARNING_DEV = "CMake Warning (dev)"; //$NON-NLS-1$

    @Override
    String getMessageStart() {
      return START_WARNING_DEV;
    }

    @Override
    int getSeverity() {
      return IMarker.SEVERITY_WARNING;
    }

    @Override
    public String toString() {
      return super.toString() + ": " + getMessageStart(); //$NON-NLS-1$
    }
  }
}
