/**
 * Copyright 2009 Google Inc.
 *
 * 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 org.waveprotocol.wave.model.document.operation.util;

import org.waveprotocol.wave.model.util.Pair;
import org.waveprotocol.wave.model.util.Preconditions;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

public abstract class ImmutableUpdateMap<T extends ImmutableUpdateMap<T, U>, U extends UpdateMap>
    implements UpdateMap {

  public static class AttributeUpdate {
    public final String name;
    final String oldValue;
    final String newValue;

    public AttributeUpdate(String name, String oldValue, String newValue) {
      Preconditions.checkNotNull(name, "Null name in AttributeUpdate");
      this.name = name;
      this.oldValue = oldValue;
      this.newValue = newValue;
    }

    @Override
    public String toString() {
      return "[" + name + ": " + oldValue + " -> " + newValue + "]";
    }
  }

  protected final List<AttributeUpdate> updates;

  @Override
  public int changeSize() {
    return updates.size();
  }

  @Override
  public String getChangeKey(int i) {
    return updates.get(i).name;
  }

  @Override
  public String getOldValue(int i) {
    return updates.get(i).oldValue;
  }

  @Override
  public String getNewValue(int i) {
    return updates.get(i).newValue;
  }

  public ImmutableUpdateMap() {
    updates = Collections.emptyList();
  }

  public ImmutableUpdateMap(String ... triples) {
    Preconditions.checkArgument(triples.length % 3 == 0, "Triples must come in groups of three");

    ArrayList<AttributeUpdate> accu = new ArrayList<AttributeUpdate>(triples.length / 3);
    for (int i = 0; i < triples.length; i += 3) {
      Preconditions.checkNotNull(triples[i], "Null key");
      accu.add(new AttributeUpdate(triples[i], triples[i + 1], triples[i + 2]));
    }

    Collections.sort(accu, comparator);

    for (int i = 1; i < accu.size(); i++) {
      int x = comparator.compare(accu.get(i - 1), accu.get(i));
      if (x == 0) {
        throw new IllegalArgumentException("Duplicate key: " + accu.get(i).name);
      }
      assert x < 0;
    }

    updates = accu;
  }

  public ImmutableUpdateMap(Map<String, Pair<String, String>> updates) {
    this(tripletsFromMap(updates));
  }

  private static String[] tripletsFromMap(Map<String, Pair<String, String>> updates) {
    String[] triplets = new String[updates.size() * 3];
    int i = 0;
    for (Map.Entry<String, Pair<String, String>> e : updates.entrySet()) {
      triplets[i++] = e.getKey();
      triplets[i++] = e.getValue().getFirst();
      triplets[i++] = e.getValue().getSecond();
    }
    return triplets;
  }

  protected ImmutableUpdateMap(List<AttributeUpdate> updates) {
    this.updates = updates;
  }

  public T exclude(Collection<String> names) {
    List<AttributeUpdate> newAttributes = new ArrayList<AttributeUpdate>();
    for (AttributeUpdate update : updates) {
      if (!names.contains(update.name)) {
        newAttributes.add(update);
      }
    }
    return createFromList(newAttributes);
  }

  protected static final Comparator<AttributeUpdate> comparator =
      new Comparator<AttributeUpdate>() {
        @Override
        public int compare(AttributeUpdate a, AttributeUpdate b) {
          return a.name.compareTo(b.name);
        }
      };

  public T composeWith(U mutation) {
    List<AttributeUpdate> newAttributes = new ArrayList<AttributeUpdate>();
    Iterator<AttributeUpdate> iterator = updates.iterator();
    AttributeUpdate nextAttribute = iterator.hasNext() ? iterator.next() : null;
    // TODO: Have a slow path when the cast would fail.
    List<AttributeUpdate> mutationAttributes = ((ImmutableUpdateMap<?,?>) mutation).updates;
    loop: for (AttributeUpdate attribute : mutationAttributes) {
      while (nextAttribute != null) {
        int comparison = comparator.compare(attribute, nextAttribute);
        if (comparison < 0) {
          break;
        } else if (comparison > 0) {
          newAttributes.add(nextAttribute);
          nextAttribute = iterator.hasNext() ? iterator.next() : null;
        } else {
          if (!areEqual(nextAttribute.newValue, attribute.oldValue)) {
            Preconditions.illegalArgument(
                "Mismatched old value: attempt to update " + nextAttribute + " with " + attribute);
          }
          newAttributes.add(new AttributeUpdate(attribute.name, nextAttribute.oldValue,
              attribute.newValue));
          nextAttribute = iterator.hasNext() ? iterator.next() : null;
          continue loop;
        }
      }
      newAttributes.add(attribute);
    }
    if (nextAttribute != null) {
      newAttributes.add(nextAttribute);
      while (iterator.hasNext()) {
        newAttributes.add(iterator.next());
      }
    }
    return createFromList(newAttributes);
  }

  protected abstract T createFromList(List<AttributeUpdate> attributes);

  // TODO: Is there a utility method for this somewhere?
  private boolean areEqual(Object a, Object b) {
    return (a == null) ? b == null : a.equals(b);
  }

  @Override
  public String toString() {
    return "Updates: " + updates;
  }

  public static void checkUpdatesSorted(List<AttributeUpdate> updates) {
    AttributeUpdate previous = null;
    for (AttributeUpdate u : updates) {
      Preconditions.checkNotNull(u, "Null attribute update");
      assert u.name != null;
      if (previous != null && previous.name.compareTo(u.name) >= 0) {
        Preconditions.illegalArgument(
            "Attribute keys not strictly monotonic: " + previous.name + ", " + u.name);
      }
      previous = u;
    }
  }

}
