/*
 * 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 consulo.ide.impl.idea.openapi.diff.impl.patch.apply;

import consulo.ide.impl.idea.openapi.diff.impl.patch.ApplyPatchException;
import consulo.ide.impl.idea.openapi.diff.impl.patch.ApplyPatchStatus;
import consulo.ide.impl.idea.openapi.diff.impl.patch.PatchHunk;
import consulo.ide.impl.idea.openapi.diff.impl.patch.PatchLine;

import java.util.ArrayList;
import java.util.List;

public class ApplyPatchHunk {
  private final PatchHunk myHunk;

  public ApplyPatchHunk(final PatchHunk hunk) {
    myHunk = hunk;
  }
  
  public ApplyPatchStatus apply(final List<String> lines) throws ApplyPatchException {
    List<String> originalLines = new ArrayList<String>(lines);
    try {
      return tryApply(lines, false);
    }
    catch(ApplyPatchException ex) {
      lines.clear();
      lines.addAll(originalLines);
      return tryApply(lines, true);
    }
  }

  private ApplyPatchStatus tryApply(final List<String> lines, boolean acceptPartial) throws ApplyPatchException {
    final List<PatchLine> hunkLines = myHunk.getLines();
    ApplyPatchStatus result = null;
    int curLine = findStartLine(hunkLines, lines);
    for(PatchLine line: hunkLines) {
      final String patchLineText = line.getText();
      switch (line.getType()) {
        case CONTEXT:
          checkContextMismatch(lines, curLine, patchLineText);
          curLine++;
          break;

        case ADD:
          if (curLine < lines.size() && lines.get(curLine).equals(patchLineText) && acceptPartial) {
            result = ApplyPatchStatus.and(result, ApplyPatchStatus.ALREADY_APPLIED);
          }
          else {
            lines.add(curLine, patchLineText);
            result = ApplyPatchStatus.and(result, ApplyPatchStatus.SUCCESS);
          }
          curLine++;
          break;

        case REMOVE:
          if (curLine >= lines.size() || !patchLineText.equals(lines.get(curLine))) {
            if (acceptPartial) {
              // we'll get a context mismatch exception later if it's actually a conflict and not an already applied line
              result = ApplyPatchStatus.and(result, ApplyPatchStatus.ALREADY_APPLIED);
            }
            else {
              checkContextMismatch(lines, curLine, patchLineText);
            }
          }
          else {
            lines.remove(curLine);
            result = ApplyPatchStatus.and(result, ApplyPatchStatus.SUCCESS);
          }
          break;
      }
    }
    if (result != null) {
      return result;
    }
    return ApplyPatchStatus.SUCCESS;
  }

  private static void checkContextMismatch(final List<String> lines, final int curLine, final String patchLineText) throws ApplyPatchException {
    if (curLine >= lines.size()) {
      throw new ApplyPatchException("Unexpected end of document. Expected line:\n" + patchLineText);
    }
    if (!patchLineText.equals(lines.get(curLine))) {
      throw new ApplyPatchException("Context mismatch. Expected line:\n" + patchLineText + "\nFound line:\n" + lines.get(curLine));
    }
  }

  private int findStartLine(final List<PatchLine> hunkLines, final List<String> lines) throws ApplyPatchException {
    int totalContextLines = countContextLines(hunkLines);
    final int startLineBefore = myHunk.getStartLineBefore();
    if (getLinesProcessingContext(hunkLines, lines, startLineBefore) == totalContextLines) {
      return startLineBefore;
    }
    int maxContextStartLine = -1;
    int maxContextLines = 0;
    for(int i=0;i< lines.size(); i++) {
      int contextLines = getLinesProcessingContext(hunkLines, lines, i);
      if (contextLines == totalContextLines) {
        return i;
      }
      if (contextLines > maxContextLines) {
        maxContextLines = contextLines;
        maxContextStartLine = i;
      }
    }
    if (maxContextLines < 2) {
      throw new ApplyPatchException("couldn't find context");
    }
    return maxContextStartLine;
  }

  private int countContextLines(final List<PatchLine> hunkLines) {
    int count = 0;
    for(PatchLine line: hunkLines) {
      if (line.getType() == PatchLine.Type.CONTEXT || line.getType() == PatchLine.Type.REMOVE) {
        count++;
      }
    }
    return count;
  }

  private int getLinesProcessingContext(final List<PatchLine> hunkLines, final List<String> lines, int startLine) {
    int count = 0;
    for(PatchLine line: hunkLines) {
      PatchLine.Type type = line.getType();
      if (type == PatchLine.Type.REMOVE || type == PatchLine.Type.CONTEXT) {
        // TODO: smarter algorithm (search outward from non-context lines)
        if (startLine >= lines.size() || !line.getText().equals(lines.get(startLine))) {
          return count;
        }
        count++;
        startLine++;
      }
    }
    return count;
  }
}
