package com.wavebang.testing;

import java.io.FileOutputStream;
import java.io.PrintStream;
import java.util.Map;

import org.waveprotocol.wave.model.document.bootstrap.BootstrapDocument;
import org.waveprotocol.wave.model.document.operation.AnnotationBoundaryMap;
import org.waveprotocol.wave.model.document.operation.Attributes;
import org.waveprotocol.wave.model.document.operation.AttributesUpdate;
import org.waveprotocol.wave.model.document.operation.DocOp;
import org.waveprotocol.wave.model.document.operation.DocOpCursor;
import org.waveprotocol.wave.model.document.operation.automaton.DocumentSchema;
import org.waveprotocol.wave.model.document.operation.impl.DocOpUtil;
import org.waveprotocol.wave.model.document.operation.impl.DocOpValidator;
import org.waveprotocol.wave.model.testing.RandomDocOpGenerator;
import org.waveprotocol.wave.model.testing.RandomProviderImpl;
import org.waveprotocol.wave.model.testing.RandomDocOpGenerator.Parameters;
import org.waveprotocol.wave.model.testing.RandomDocOpGenerator.RandomProvider;

public class RandomOpSerializer {

  private static String literalString(String string) {
    return (string == null) ? "null" : "\"" + escapeLiteral(string) + "\"";
  }

  private static String escapeLiteral(String string) {
    return string.replace("\\", "\\\\").replace("\"", "\\\"");
  }
  
  public static String toJsonString(Attributes attributes) {
    if (attributes.isEmpty()) {
      return "[]";
    }
    StringBuilder b = new StringBuilder();
    b.append("[");
    boolean first = true;
    for (Map.Entry<String, String> entry : attributes.entrySet()) {
      if (first) {
        first = false;
      } else {
        b.append(",");
      }
      b.append("{\"1\":");
      b.append(literalString(entry.getKey()));
      b.append(",\"2\":");
      b.append(literalString(entry.getValue()));
      b.append("}");
    }
    b.append("]");
    return b.toString();
  }

  public static String toJsonString(AttributesUpdate update) {
    if (update.changeSize() == 0) {
      return "[]";
    }
    StringBuilder b = new StringBuilder();
    b.append("[");
    for (int i = 0; i < update.changeSize(); ++i) {
      if (i > 0) {
        b.append(",");
      }

      b.append("{\"1\":");
      b.append(literalString(update.getChangeKey(i)));
      if (update.getOldValue(i) != null) {
        b.append(",\"2\":" + literalString(update.getOldValue(i)));
      }
      if (update.getNewValue(i) != null) {
        b.append(",\"3\":" + literalString(update.getNewValue(i)));
      }
      b.append("}");
    }
    b.append("]");
    return b.toString();
  }
  
  // Eg: [{"3":{"1":"conversation"}},{"4":true}]
  public static DocOpCursor createJsonSerializer(DocOp op, final StringBuilder b) {
    return new DocOpCursor() {
      boolean start = true;
      public void readyNextElement(int key) {
        if (start) {
          start = false;
        } else {
          b.append("},{");
        }
        
        b.append("\"" + key + "\":");
      }
      
      @Override
      public void annotationBoundary(AnnotationBoundaryMap map) {
        readyNextElement(1);
        b.append("{\"2\":[");
        for (int i = 0; i < map.endSize(); i++) {
          if (i > 0) {
            b.append(",");
          }
          b.append(literalString(map.getEndKey(i)));
        }
        b.append("],\"3\":[");
        for (int i = 0; i < map.changeSize(); ++i) {
          if (i > 0) {
            b.append(",");
          }

          b.append("{\"1\":");
          b.append(literalString(map.getChangeKey(i)));
          if (map.getOldValue(i) != null) {
            b.append(",\"2\":" + literalString(map.getOldValue(i)));
          }
          if (map.getNewValue(i) != null) {
            b.append(",\"3\":" + literalString(map.getNewValue(i)));
          }
          b.append("}");
        }
        b.append("]}");
      }

      @Override
      public void characters(String chars) {
        readyNextElement(2);
        b.append(literalString(chars));
      }
      
      @Override
      public void deleteCharacters(String chars) {
        readyNextElement(6);
        b.append(literalString(chars));
      }

      @Override
      public void deleteElementEnd() {
        readyNextElement(8);
        b.append("true");
      }

      public void printElementStart(String type, Attributes attrs) {
        b.append("{\"1\":\"" + type + "\"");
        if (!attrs.isEmpty()) {
          b.append(",\"2\":");
          b.append(toJsonString(attrs));
        }
        b.append("}");     
      }
      
      @Override
      public void deleteElementStart(String type, Attributes attrs) {
        readyNextElement(7);
        printElementStart(type, attrs);
      }

      @Override
      public void replaceAttributes(Attributes oldAttrs, Attributes newAttrs) {
        readyNextElement(9);
        b.append("{\"2\":" + toJsonString(oldAttrs) + ",\"3\":" + toJsonString(newAttrs) + "}");
      }

      @Override
      public void retain(int distance) {
        readyNextElement(5);
        b.append(distance);
      }

      @Override
      public void updateAttributes(AttributesUpdate attrUpdate) {
        readyNextElement(10);
        b.append("{\"2\":" + toJsonString(attrUpdate) + "}");
      }

      @Override
      public void elementEnd() {
        readyNextElement(4);
        b.append("true");
      }

      @Override
      public void elementStart(String type, Attributes attrs) {
        readyNextElement(3);
        printElementStart(type, attrs);
      }
    };
  }

  public static String serialize(DocOp op) {
    final StringBuilder b = new StringBuilder();
    op.apply(createJsonSerializer(op, b));
    return "{\"1\":[{" + b.toString() + "}]}";
  }
  
  public static void main(String[] args) throws Exception {
    BootstrapDocument initialDoc = new BootstrapDocument();
    
    initialDoc.consume(new DocOp() {
      @Override
      public void apply(DocOpCursor c) {
        c.elementStart("blip", Attributes.EMPTY_MAP);
        c.elementStart("p", Attributes.EMPTY_MAP);
        c.characters("abc");
        c.elementEnd();
        c.elementEnd();
      }
    });

    Parameters p = new Parameters();

    p.setMaxOpeningComponents(10);

    RandomProvider r = RandomProviderImpl.ofSeed(2538);
    for (int i = 0; i < 100; i++) {
      String filename = "RandomOps" + i + ".txt";
      System.out.print("Generating random ops into " + filename + " ");
      PrintStream file = new PrintStream(new FileOutputStream(filename));
      BootstrapDocument doc = new BootstrapDocument();
      doc.consume(initialDoc.asOperation());
      file.println(serialize(doc.asOperation()));
      file.println(DocOpUtil.toXmlString(doc.asOperation()));
      for (int j = 0; j < 200; j++) {
        DocOp op = RandomDocOpGenerator.generate(r, p, doc);
        doc.consume(op);
        file.println(serialize(op));
        file.println(DocOpUtil.toXmlString(doc.asOperation()));
//        System.out.println(serialize(doc.asOperation()));
        
//        System.err.println("new: " + DocOpUtil.toConciseString(doc.asOperation()));
//        System.err.println("new: " + DocOpUtil.toXmlString(doc.asOperation()));
        if (!DocOpValidator.validate(null, DocumentSchema.NO_SCHEMA_CONSTRAINTS,
            doc.asOperation()).isValid()) {
          throw new RuntimeException("doc not valid");
        }
        if ((j - 1) % 40 == 0) {
          System.out.print(".");
        }
      }
      System.out.println(" done");
    }
  }
}
