/*
 * Copyright 2000-2009 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 com.intellij.history.core;

import com.intellij.history.ByteContent;
import com.intellij.history.core.changes.*;
import com.intellij.history.core.revisions.ChangeRevision;
import com.intellij.history.core.revisions.RecentChange;
import com.intellij.history.core.revisions.Revision;
import com.intellij.history.core.tree.Entry;
import com.intellij.history.core.tree.RootEntry;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.NlsContexts;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.TestOnly;

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

public class LocalHistoryFacade {
  private final ChangeList myChangeList;
  private final List<Listener> myListeners = ContainerUtil.createLockFreeCopyOnWriteList();

  public LocalHistoryFacade(ChangeList changeList) {
    myChangeList = changeList;
  }

  public void beginChangeSet() {
    myChangeList.beginChangeSet();
  }

  public void forceBeginChangeSet() {
    if (myChangeList.forceBeginChangeSet()) {
      fireChangeSetFinished();
    }
  }

  public void endChangeSet(@NlsContexts.Label String name) {
    if (myChangeList.endChangeSet(name)) {
      fireChangeSetFinished();
    }
  }

  public void created(String path, boolean isDirectory) {
    addChange(isDirectory ? new CreateDirectoryChange(myChangeList.nextId(), path)
                          : new CreateFileChange(myChangeList.nextId(), path));
  }

  public void contentChanged(String path, Content oldContent, long oldTimestamp) {
    addChange(new ContentChange(myChangeList.nextId(), path, oldContent, oldTimestamp));
  }

  public void renamed(String path, String oldName) {
    addChange(new RenameChange(myChangeList.nextId(), path, oldName));
  }

  public void readOnlyStatusChanged(String path, boolean oldStatus) {
    addChange(new ROStatusChange(myChangeList.nextId(), path, oldStatus));
  }

  public void moved(String path, String oldParent) {
    addChange(new MoveChange(myChangeList.nextId(), path, oldParent));
  }

  public void deleted(String path, Entry deletedEntry) {
    addChange(new DeleteChange(myChangeList.nextId(), path, deletedEntry));
  }

  public LabelImpl putSystemLabel(@NlsContexts.Label String name, String projectId, int color) {
    return putLabel(new PutSystemLabelChange(myChangeList.nextId(), name, projectId, color));
  }

  public LabelImpl putUserLabel(@NlsContexts.Label String name, String projectId) {
    return putLabel(new PutLabelChange(myChangeList.nextId(), name, projectId));
  }

  private void addChange(@NotNull Change c) {
    beginChangeSet();
    myChangeList.addChange(c);
    fireChangeAdded(c);
    endChangeSet(null);
  }

  @TestOnly
  public void addChangeInTests(@NotNull StructuralChange c) {
    addChange(c);
  }

  private LabelImpl putLabel(final @NotNull PutLabelChange c) {
    addChange(c);
    return new LabelImpl() {

      @Override
      public long getLabelChangeId() {
        return c.getId();
      }

      @Override
      public ByteContent getByteContent(RootEntry root, String path) {
        return getByteContentBefore(root, path, c);
      }
    };
  }

  @TestOnly
  public void putLabelInTests(final PutLabelChange c) {
    putLabel(c);
  }

  @TestOnly
  public ChangeList getChangeListInTests() {
    return myChangeList;
  }

  private ByteContent getByteContentBefore(RootEntry root, String path, Change change) {
    root = root.copy();
    String newPath = revertUpTo(root, path, null, change, false, false);
    Entry entry = root.findEntry(newPath);
    if (entry == null) return new ByteContent(false, null);
    if (entry.isDirectory()) return new ByteContent(true, null);

    return new ByteContent(false, entry.getContent().getBytesIfAvailable());
  }

  public List<RecentChange> getRecentChanges(RootEntry root) {
    List<RecentChange> result = new ArrayList<>();

    for (ChangeSet c : myChangeList.iterChanges()) {
      if (c.isContentChangeOnly()) continue;
      if (c.isLabelOnly()) continue;
      if (c.getName() == null) continue;

      Revision before = new ChangeRevision(this, root, "", c, true);
      Revision after = new ChangeRevision(this, root, "", c, false);
      result.add(new RecentChange(before, after));
      if (result.size() >= 20) break;
    }

    return result;
  }

  public void accept(ChangeVisitor v) {
    myChangeList.accept(v);
  }

  public String revertUpTo(final @NotNull RootEntry root,
                           @NotNull String path,
                           final ChangeSet targetChangeSet,
                           final Change targetChange,
                           final boolean revertTargetChange,
                           final boolean warnOnFileNotFound) {
    final String[] result = {path};
    myChangeList.accept(new ChangeVisitor() {
      @Override
      public void begin(ChangeSet c) throws StopVisitingException {
        if (!revertTargetChange && c.equals(targetChangeSet)) stop();
      }

      @Override
      public void end(ChangeSet c) throws StopVisitingException {
        if (c.equals(targetChangeSet)) stop();
      }

      @Override
      public void visit(PutLabelChange c) throws StopVisitingException {
        if (c.equals(targetChange)) stop();
      }

      @Override
      public void visit(StructuralChange c) throws StopVisitingException {
        if (!revertTargetChange && c.equals(targetChange)) stop();

        if (c.affectsPath(result[0])) {
          c.revertOn(root, warnOnFileNotFound);
          result[0] = c.revertPath(result[0]);
        }
        if (c.equals(targetChange)) stop();
      }
    });

    return result[0];
  }

  public void addListener(final @NotNull Listener l, @Nullable Disposable parent) {
    myListeners.add(l);

    if (parent != null) {
      Disposer.register(parent, new Disposable() {
        @Override
        public void dispose() {
          myListeners.remove(l);
        }
      });
    }
  }

  public void removeListener(@NotNull Listener l) {
    myListeners.remove(l);
  }

  private void fireChangeAdded(Change c) {
    for (Listener each : myListeners) {
      each.changeAdded(c);
    }
  }

  private void fireChangeSetFinished() {
    for (Listener each : myListeners) {
      each.changeSetFinished();
    }
  }

  public abstract static class Listener {
    public void changeAdded(Change c) {
    }

    public void changeSetFinished() {
    }
  }
}
