// Copyright 2012 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.collide.shared.ot;

import com.google.collide.dto.DocOp;
import com.google.collide.dto.DocOpComponent;
import com.google.collide.dto.shared.DocOpFactory;
import com.google.collide.json.shared.JsonArray;
import com.google.common.base.Preconditions;

/*
 * Influenced by Wave's Transformer and Composer classes. Generally, we can't
 * use Wave's because we introduce the RetainLine doc op component, and don't
 * support a few extra components that Wave has. We didn't fork Wave's
 * Transformer because its design wasn't amenable to the changes required by
 * RetainLine. Instead, we wrote this from scratch to be able to handle that
 * component easily.
 *
 * The operations being transformed are A and B. Both are intended to be applied
 * to the same document. The output of the transformation will be A' and B'. For
 * example, A' will be A transformed so it can be cleanly applied after B has
 * been applied.
 *
 * The Processor class maintains the state for a document operation component.
 * Each method in the Processor handles a component from the other document
 * operation. As each method is executed, it outputs to its output document
 * operation and to the other processor's output document operation. It also
 * marks the general state into the ProcessorResult.
 */
/**
 * Transforms document operations for the code editor.
 *
 */
public class Transformer {

  /**
   * Exception that is thrown when there is a problem transforming two document
   * operations.
   */
  public static class TransformException extends RuntimeException {
    /**
     * 
     */
    private static final long serialVersionUID = 5029139051395933161L;

    public TransformException(String message, Throwable cause) {
      super(message, cause);
    }
  }

  private static class DeleteProcessor extends Processor {

    static String performDelete(DocOpCapturer output, String text, int deleteLength,
        ProcessorResult result) {

      output.delete(text.substring(0, deleteLength));

      if (text.length() == deleteLength) {
        result.markMyStateFinished();
      }

      return text.substring(deleteLength);
    }

    private String text;

    DeleteProcessor(DocOpCapturer output, String text) {
      super(output);
      this.text = text;
    }

    @Override
    void handleOtherDelete(DeleteProcessor other, ProcessorResult result) {
      /*
       * The transformed op shouldn't know about the deletes, so don't output
       * anything
       */
      if (text.length() == other.text.length()) {
        result.markMyStateFinished();
        result.markOtherStateFinished();
      } else if (text.length() < other.text.length()) {
        other.text = other.text.substring(text.length());
        result.markMyStateFinished();
      } else {
        text = text.substring(other.text.length());
        result.markOtherStateFinished();
      }
    }

    @Override
    void handleOtherFinished(Processor other, ProcessorResult result) {
      throw new IllegalStateException("Cannot delete if the other side is finished");
    }

    @Override
    void handleOtherInsert(InsertProcessor other, ProcessorResult result) {
      /*
       * Look at comments in InsertProcessor on why it needs to always handle
       * these components
       */
      result.flip();
      other.handleOtherDelete(this, result);
    }

    @Override
    void handleOtherRetain(RetainProcessor other, ProcessorResult result) {
      /*
       * The other transformed op won't have anything to retain, so output
       * nothing for it. Our transformed op needs to delete though, since the
       * other original op is just retaining.
       */
      int minCount = Math.min(text.length(), other.count);
      other.count -= minCount;

      text = performDelete(output, text, minCount, result);

      if (other.count == 0) {
        result.markOtherStateFinished();
      }
    }

    @Override
    void handleOtherRetainLine(RetainLineProcessor other, ProcessorResult result) {
      // Let RetainLineProcessor handle this
      result.flip();
      other.handleOtherDelete(this, result);
    }
  }

  private static class FinishedProcessor extends Processor {
    FinishedProcessor(DocOpCapturer output) {
      super(output);
    }

    @Override
    void handleOtherDelete(DeleteProcessor other, ProcessorResult result) {
      result.flip();
      other.handleOtherFinished(this, result);
    }

    @Override
    void handleOtherFinished(Processor other, ProcessorResult result) {
      throw new IllegalStateException("Both should not be finished");
    }

    @Override
    void handleOtherInsert(InsertProcessor other, ProcessorResult result) {
      result.flip();
      other.handleOtherFinished(this, result);
    }

    @Override
    void handleOtherRetain(RetainProcessor other, ProcessorResult result) {
      result.flip();
      other.handleOtherFinished(this, result);
    }

    @Override
    void handleOtherRetainLine(RetainLineProcessor other, ProcessorResult result) {
      result.flip();
      other.handleOtherFinished(this, result);
    }
  }

  private static class InsertProcessor extends Processor {

    private static void performInsert(DocOpCapturer output, DocOpCapturer otherOutput,
        String text, ProcessorResult result) {
      output.insert(text);
      boolean endsWithNewline = text.endsWith(NEWLINE);
      otherOutput.retain(text.length(), endsWithNewline);
      
      if (endsWithNewline) {
        result.markMyCurrentComponentInsertOfNewline();
      }
      
      result.markMyStateFinished();
    }

    private String text;

    InsertProcessor(DocOpCapturer output, String text) {
      super(output);
      this.text = text;
    }

    @Override
    void handleOtherDelete(DeleteProcessor other, ProcessorResult result) {
      // Handle insertion
      performInsert(output, other.output, text, result);

      // The delete will be handled by the successor for this processor
    }

    @Override
    void handleOtherFinished(Processor other, ProcessorResult result) {
      performInsert(output, other.output, text, result);
    }

    @Override
    void handleOtherInsert(InsertProcessor other, ProcessorResult result) {
      /*
       * Instead of inserting both, we only insert one so contiguous insertions
       * by one side will end up being contiguous in the transformed op.
       * (Otherwise, you get interleaved I, RL, I, RL, ...)
       */
      performInsert(output, other.output, text, result);
    }

    @Override
    void handleOtherRetain(RetainProcessor other, ProcessorResult result) {
      performInsert(output, other.output, text, result);

      // The retain will be handled by the successor for this processor
    }

    @Override
    void handleOtherRetainLine(RetainLineProcessor other, ProcessorResult result) {
      result.flip();
      other.handleOtherInsert(this, result);
    }
  }

  private abstract static class Processor {
    final DocOpCapturer output;

    Processor(DocOpCapturer output) {
      this.output = output;
    }

    abstract void handleOtherDelete(DeleteProcessor other, ProcessorResult result);

    abstract void handleOtherFinished(Processor other, ProcessorResult result);

    abstract void handleOtherInsert(InsertProcessor other, ProcessorResult result);

    abstract void handleOtherRetain(RetainProcessor other, ProcessorResult result);

    abstract void handleOtherRetainLine(RetainLineProcessor other, ProcessorResult result);
  }

  private static class ProcessorFactory implements DocOpCursor {

    private DocOpCapturer curOutput;

    private Processor returnProcessor;

    @Override
    public void delete(String text) {
      returnProcessor = new DeleteProcessor(curOutput, text);
    }

    @Override
    public void insert(String text) {
      returnProcessor = new InsertProcessor(curOutput, text);
    }

    @Override
    public void retain(int count, boolean hasTrailingNewline) {
      returnProcessor = new RetainProcessor(curOutput, count, hasTrailingNewline);
    }

    @Override
    public void retainLine(int lineCount) {
      returnProcessor = new RetainLineProcessor(curOutput, lineCount);
    }

    Processor create(DocOpCapturer output, DocOpComponent component) {
      curOutput = output;
      DocOpUtils.acceptComponent(component, this);
      return returnProcessor;
    }
  }

  private static class ProcessorResult {
    private boolean isMyStateFinished;
    private boolean isOtherStateFinished;
    
    /*
     * We need to know the value of the previous component, but if we only tracked that then reset
     * would clear it.
     */
    private boolean isMyCurrentComponentInsertOfNewline;
    private boolean isOtherCurrentComponentInsertOfNewline;
    private boolean isMyPreviousComponentInsertOfNewline;
    private boolean isOtherPreviousComponentInsertOfNewline;


    private boolean isFlipped;

    private ProcessorResult() {
    }

    /**
     * Flips the "my" and "other" states. This should be called before one
     * processor is handing over execution to the other processor, including
     * passing this instance to the other processor.
     */
    void flip() {
      boolean origMyStateFinished = isMyStateFinished;
      isMyStateFinished = isOtherStateFinished;
      isOtherStateFinished = origMyStateFinished;

      boolean origMyPreviousComponentInsertOfNewline = isMyPreviousComponentInsertOfNewline;
      isMyPreviousComponentInsertOfNewline = isOtherPreviousComponentInsertOfNewline;
      isOtherPreviousComponentInsertOfNewline = origMyPreviousComponentInsertOfNewline;
      
      boolean origMyCurrentComponentInsertOfNewline = isMyCurrentComponentInsertOfNewline;
      isMyCurrentComponentInsertOfNewline = isOtherCurrentComponentInsertOfNewline;
      isOtherCurrentComponentInsertOfNewline = origMyCurrentComponentInsertOfNewline;
      
      isFlipped = !isFlipped;
    }

    void markMyStateFinished() {
      if (!isFlipped) {
        isMyStateFinished = true;
      } else {
        isOtherStateFinished = true;
      }
    }

    void markOtherStateFinished() {
      if (!isFlipped) {
        isOtherStateFinished = true;
      } else {
        isMyStateFinished = true;
      }
    }
    
    void markMyCurrentComponentInsertOfNewline() {
      if (!isFlipped) {
        isMyCurrentComponentInsertOfNewline = true;
      } else {
        isOtherCurrentComponentInsertOfNewline = true;
      }
    }

    void markOtherCurrentComponentInsertOfNewline() {
      if (!isFlipped) {
        isOtherCurrentComponentInsertOfNewline = true;
      } else {
        isMyCurrentComponentInsertOfNewline = true;
      }
    }

    void reset() {
      isMyPreviousComponentInsertOfNewline = isMyCurrentComponentInsertOfNewline;
      isOtherPreviousComponentInsertOfNewline = isOtherCurrentComponentInsertOfNewline;
      
      isFlipped = isMyStateFinished = isOtherStateFinished =
          isMyCurrentComponentInsertOfNewline = isOtherCurrentComponentInsertOfNewline = false;
    }
  }

  private static class RetainLineProcessor extends Processor {
    private int lineCount;

    /**
     * In the event that we need to expand the retain line, we need to know
     * exactly how many retains it should be expanded to. This tracks that
     * number.
     */
    private int substituteRetainCount;

    RetainLineProcessor(DocOpCapturer output, int lineCount) {
      super(output);
      this.lineCount = lineCount;
    }

    @Override
    void handleOtherDelete(DeleteProcessor other, ProcessorResult result) {
      other.output.delete(other.text);

      if (other.text.endsWith(NEWLINE)) {
        handleOtherLineEnd(false, result);
      } else {
        // My transformed op won't see the delete, so do nothing
      }

      result.markOtherStateFinished();
    }

    @Override
    void handleOtherFinished(Processor other, ProcessorResult result) {
      Preconditions.checkState(
          lineCount == 1, "Cannot retain more than one line if other side is finished");

      if (result.isMyPreviousComponentInsertOfNewline) {
        other.output.retainLine(1);
      }
      
      lineCount = 0;
      output.retainLine(1);
      result.markMyStateFinished();
    }

    @Override
    void handleOtherInsert(InsertProcessor other, ProcessorResult result) {
      other.output.insert(other.text);

      if (other.text.endsWith(NEWLINE)) {
        // Retain the line just inserted by other
        lineCount++;
        handleOtherLineEnd(true, result);
        
        result.markOtherCurrentComponentInsertOfNewline();
        
      } else {
        substituteRetainCount += other.text.length();
      }

      result.markOtherStateFinished();
    }

    void handleOtherLineEnd(boolean canUseRetainLine, ProcessorResult result) {
      if (canUseRetainLine) {
        output.retainLine(1);
      } else {
        if (substituteRetainCount > 0) {
          output.retain(substituteRetainCount, false);
        }
      }

      lineCount--;
      substituteRetainCount = 0;

      if (lineCount == 0) {
        result.markMyStateFinished();
      }
    }

    @Override
    void handleOtherRetain(RetainProcessor other, ProcessorResult result) {
      other.output.retain(other.count, other.hasTrailingNewline);
      substituteRetainCount += other.count;

      if (other.hasTrailingNewline) {
        handleOtherLineEnd(true, result);
      }

      result.markOtherStateFinished();
    }

    @Override
    void handleOtherRetainLine(RetainLineProcessor other, ProcessorResult result) {
      int minLineCount = Math.min(lineCount, other.lineCount);

      output.retainLine(minLineCount);
      lineCount -= minLineCount;

      other.output.retainLine(minLineCount);
      other.lineCount -= minLineCount;

      if (lineCount == 0) {
        result.markMyStateFinished();
      }

      if (other.lineCount == 0) {
        result.markOtherStateFinished();
      }
    }
  }

  private static class RetainProcessor extends Processor {

    static int performRetain(DocOpCapturer output, int fullCount, int retainCount,
        boolean hasTrailingNewline, ProcessorResult result, boolean useOtherInResult) {
      output.retain(retainCount, fullCount == retainCount ? hasTrailingNewline : false);

      if (retainCount == fullCount) {
        if (useOtherInResult) {
          result.markOtherStateFinished();
        } else {
          result.markMyStateFinished();
        }
      }

      return fullCount - retainCount;
    }

    private int count;
    private final boolean hasTrailingNewline;

    RetainProcessor(DocOpCapturer output, int count, boolean hasTrailingNewline) {
      super(output);
      this.count = count;
      this.hasTrailingNewline = hasTrailingNewline;
    }

    @Override
    void handleOtherDelete(DeleteProcessor other, ProcessorResult result) {
      result.flip();
      other.handleOtherRetain(this, result);
    }

    @Override
    void handleOtherFinished(Processor other, ProcessorResult result) {
      throw new IllegalStateException("Cannot retain if other side is finished");
    }

    @Override
    void handleOtherInsert(InsertProcessor other, ProcessorResult result) {
      result.flip();
      other.handleOtherRetain(this, result);
    }

    @Override
    void handleOtherRetain(RetainProcessor other, ProcessorResult result) {
      int minCount = Math.min(count, other.count);

      count = performRetain(output, count, minCount, hasTrailingNewline, result, false);
      other.count =
          performRetain(other.output, other.count, minCount, other.hasTrailingNewline, result,
              true);
    }

    @Override
    void handleOtherRetainLine(RetainLineProcessor other, ProcessorResult result) {
      result.flip();
      other.handleOtherRetain(this, result);
    }
  }

  private static final String NEWLINE = "\n";

  private static final ProcessorFactory PROCESSOR_FACTORY = new ProcessorFactory();

  public static OperationPair transform(DocOpFactory factory, DocOp clientOp, DocOp serverOp)
      throws TransformException {
    try {
      return new Transformer(factory).transformImpl(clientOp, serverOp);
    } catch (Throwable t) {
      throw new TransformException("Could not transform doc ops:\nClient: "
          + DocOpUtils.toString(clientOp, false) + "\nServer: "
          + DocOpUtils.toString(serverOp, false) + "\n", t);
    }
  }

  private static void dispatchProcessor(Processor a, Processor b, ProcessorResult result) {
    if (b instanceof DeleteProcessor) {
      a.handleOtherDelete((DeleteProcessor) b, result);
    } else if (b instanceof InsertProcessor) {
      a.handleOtherInsert((InsertProcessor) b, result);
    } else if (b instanceof RetainProcessor) {
      a.handleOtherRetain((RetainProcessor) b, result);
    } else if (b instanceof RetainLineProcessor) {
      a.handleOtherRetainLine((RetainLineProcessor) b, result);
    } else if (b instanceof FinishedProcessor) {
      a.handleOtherFinished(b, result);
    }
  }

  private final DocOpFactory factory;

  private Transformer(DocOpFactory factory) {
    this.factory = factory;
  }

  private OperationPair transformImpl(DocOp clientOp, DocOp serverOp) {
    /*
     * These capturers will create the respective side's doc op which will be
     * transformed from the respective side's original doc op to apply to the
     * document *after* the other side's original doc op.
     */
    DocOpCapturer clientOutput = new DocOpCapturer(factory, true);
    DocOpCapturer serverOutput = new DocOpCapturer(factory, true);

    JsonArray<DocOpComponent> clientComponents = clientOp.getComponents();
    JsonArray<DocOpComponent> serverComponents = serverOp.getComponents();

    int clientIndex = 0;
    int serverIndex = 0;

    boolean clientComponentsFinished = false;
    boolean serverComponentsFinished = false;

    Processor client = null;
    Processor server = null;

    ProcessorResult result = new ProcessorResult();

    while (!clientComponentsFinished || !serverComponentsFinished) {

      if (client == null) {
        if (clientIndex < clientComponents.size()) {
          client = PROCESSOR_FACTORY.create(clientOutput, clientComponents.get(clientIndex++));
        } else {
          client = new FinishedProcessor(clientOutput);
          clientComponentsFinished = true;
        }
      }

      if (server == null) {
        if (serverIndex < serverComponents.size()) {
          server = PROCESSOR_FACTORY.create(serverOutput, serverComponents.get(serverIndex++));
        } else {
          server = new FinishedProcessor(serverOutput);
          serverComponentsFinished = true;
        }
      }

      if (!clientComponentsFinished || !serverComponentsFinished) {
        dispatchProcessor(client, server, result);
      }

      if (result.isMyStateFinished) {
        client = null;
      }

      if (result.isOtherStateFinished) {
        server = null;
      }

      result.reset();
    }

    return new OperationPair(clientOutput.getDocOp(), serverOutput.getDocOp());
  }
}
