/*
 * Copyright 2000-2010 JetBrains s.r.o.
 *
 * 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 jetbrains.buildServer.scriptedProperties;

import com.intellij.openapi.diagnostic.Logger;
import jetbrains.buildServer.parameters.ContextVariables;
import jetbrains.buildServer.parameters.ParametersProvider;
import jetbrains.buildServer.parameters.ProcessingResult;
import jetbrains.buildServer.parameters.impl.ProcessingResultImpl;
import org.jetbrains.annotations.NotNull;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Created by IntelliJ IDEA.
 * User: yaegor
 * Date: 18.09.2010
 * Time: 11:51:59
 * To change this template use File | Settings | File Templates.
 */
public class ScriptedPropertyRegistry {
  @NotNull
  private static final Logger LOG = Logger.getInstance(ScriptedPropertyProcessor.class.getName());

  private Map<String, ScriptedPropertyProcessor> myRegistry = new HashMap<String, ScriptedPropertyProcessor>();
  private static final Pattern OUR_NAME_REGEXP = Pattern.compile("[\\w.-/\\\\]*");
  private static final Pattern OUR_LONG_VALUE_REGEXP = Pattern.compile("[^\\s]*");

  public ScriptedPropertyRegistry() {
  }

  public ScriptedPropertyRegistry(Collection<ScriptedPropertyProcessor> processors) {
    myRegistry.clear();
    for (ScriptedPropertyProcessor processor : processors) {
      myRegistry.put(processor.getId(), processor);
    }
  }

  void registerScriptedPropertyType(@NotNull ScriptedPropertyProcessor processor) {
    myRegistry.put(processor.getId(), processor);
  }

  @NotNull
  public ProcessingResult resolve(@NotNull String key,
                                  @NotNull String value,
                                  @NotNull ParametersProvider parameters,
                                  @NotNull ContextVariables contextVariables,
                                  final boolean reportErrors) {
    ProcessingResultBuilder builder = new ProcessingResultBuilder(reportErrors);
    // ##id:endseq:...endseq
    // ##id{JSON}  <- NOT YET IMPLEMENTED!
    // ##id
    // "##id::value_till.:+=whitespace " <- till a whitespace
    int nextStartIndex = 0;

    while (nextStartIndex < value.length()) {
      int startIndex = value.indexOf("##", nextStartIndex);
      if (startIndex == -1) {
        builder.appendUnmodified(value, nextStartIndex, value.length());
        break;
      }
      builder.appendUnmodified(value, nextStartIndex, startIndex);

      final int idStartIndex = startIndex + "##".length();

      String id = getNameAt(value, idStartIndex);
      if (id.length() == 0) {
        nextStartIndex = startIndex + 1;
        builder.appendUnmodified(value, startIndex, nextStartIndex);
        continue;
      }

      final int endIdIndex = startIndex + "##".length() + id.length();

      ScriptedPropertyProcessor processor = getProcessorById(id);
      if (processor == null) {
        LOG.debug("Found usage of unknown scripted property: '" + id + "' while resolving property " + key + "=" + value + ". Ignoring.");
        nextStartIndex = endIdIndex;
        builder.appendError(value, startIndex, nextStartIndex, "unknown scripted property: '" + id + "'");
        continue;
      }

      if (value.length() == endIdIndex || (':' != value.charAt(endIdIndex))) {
        //##id
        nextStartIndex = endIdIndex;
        try {
          builder.appendModified(value, startIndex, nextStartIndex, processor.process(parameters, contextVariables));
        } catch (Exception e) {
          LOG.debug("Error while resolving property " + key + "=" + value + ":" + e.getMessage(), e);
          builder.appendError(value, startIndex, nextStartIndex, e.getMessage());
        }
        continue;
      }

      int secondColonIndex = value.indexOf(":", endIdIndex + 1);
      if (secondColonIndex == -1) {
        LOG.debug(
          "Scripted property '" + id + "' does not have second delimiting colon. Encountered while resolving property " + key + "=" +
          value + ".");
        nextStartIndex = endIdIndex + 1;
        builder.appendError(value, startIndex, nextStartIndex, "empty parameter text");
        continue;
      }
      String endSequence = value.substring(endIdIndex + 1, secondColonIndex);
      if (endSequence.length() == 0) {
        //##id::value_till.:+=whitespace
        final String parameterText = getValueTillWhitespace(value, secondColonIndex + 1);
        if (parameterText.length() == 0) {
          LOG.debug(
            "Scripted property '" + id + "' has empty parameter text. Encountered while resolving property " + key + "=" + value + ".");
          nextStartIndex = secondColonIndex + 1;
          builder.appendError(value, startIndex, nextStartIndex, "empty parameter text");
          continue;
        }
        nextStartIndex = secondColonIndex + parameterText.length() + 1;
        try {
          builder.appendModified(value, startIndex, nextStartIndex, processor.process(parameterText, parameters, contextVariables));
        } catch (Exception e) {
          LOG.debug("Error while resolving property " + key + "=" + value + ":" + e.getMessage(), e);
          builder.appendError(value, startIndex, nextStartIndex, e.getMessage());
        }
        continue;
      }

      //##id:seq:XXXseq
      int startOfValueIndex = secondColonIndex + 1;
      int endOfValueIndex = value.indexOf(endSequence, startOfValueIndex);
      if (endOfValueIndex == -1) {
        nextStartIndex = startOfValueIndex;
        builder.appendError(value, startIndex, nextStartIndex, "no end sequence occurrence");
        continue;
      }
      String parameterText = value.substring(startOfValueIndex, endOfValueIndex);
      nextStartIndex = endOfValueIndex + endSequence.length();
      try {
        builder.appendModified(value, startIndex, nextStartIndex, processor.process(parameterText, parameters, contextVariables));
      } catch (Exception e) {
        LOG.debug("Error while resolving property " + key + "=" + value + ":" + e.getMessage(), e);
        builder.appendError(value, startIndex, nextStartIndex, e.getMessage());
      }
    }

    return builder.getResult();
  }

  private String getValueTillWhitespace(final String text, final int fromPosition) {
    final Matcher matcher = OUR_LONG_VALUE_REGEXP.matcher(text);
    if (!matcher.find(fromPosition)) {
      return "";
    }
    return matcher.group();
  }

  private ScriptedPropertyProcessor getProcessorById(String id) {
    return myRegistry.get(id);
  }

  @NotNull
  private String getNameAt(String text, int startAt) {
    final Matcher matcher = OUR_NAME_REGEXP.matcher(text);
    if (!matcher.find(startAt)) {
      return "";
    }
    return matcher.group();
  }

  private class ProcessingResultBuilder {
    private StringBuffer myResultValue = new StringBuffer();
    private boolean myModified = false;
    private boolean myFullyResolved = true;
    private boolean myReportErrors;

    public ProcessingResultBuilder(boolean reportErrors) {
      myReportErrors = reportErrors;
    }

    public void appendUnmodified(String value, int fromIndex, int toIndex) {
      myResultValue.append(value, fromIndex, toIndex);
    }

    public void appendError(String value, int fromIndex, int toIndex, String errorMessage) {
      if (myReportErrors) {
        myResultValue.append("##<error:").append(errorMessage).append(">");
        myModified |= true;
        myFullyResolved &= false; //todo: or true?
      } else {
        appendUnmodified(value, fromIndex, toIndex);
      }
    }

    public void appendModified(String value, int fromIndex, int toIndex, ProcessingResult processingResult) {
      if (processingResult.isModified()) {
        myResultValue.append(processingResult.getResult());
        myModified = true;
        myFullyResolved &= processingResult.isFullyResolved();
      } else {
        appendUnmodified(value, fromIndex, toIndex);
      }
    }

    public ProcessingResult getResult() {
      return new ProcessingResultImpl(myResultValue.toString(), myModified, myFullyResolved);
    }

    @Override
    public String toString() {
      return "ProcessingResultBuilder{value=" + myResultValue + ", modified=" + myModified + ", fullyResolved=" + myFullyResolved + "}";
    }
  }
}
