/*
 * Copyright 2000-2015 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.openapi.editor.impl;

import com.intellij.codeStyle.CodeStyleFacade;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.EditorFactory;
import com.intellij.openapi.editor.LazyRangeMarkerFactory;
import com.intellij.openapi.editor.RangeMarker;
import com.intellij.openapi.editor.event.DocumentAdapter;
import com.intellij.openapi.editor.event.DocumentEvent;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Computable;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.UserDataHolderBase;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.containers.WeakList;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.List;

public class LazyRangeMarkerFactoryImpl extends LazyRangeMarkerFactory {
  private final Project myProject;
  private static final Key<WeakList<LazyMarker>> LAZY_MARKERS_KEY = Key.create("LAZY_MARKERS_KEY");

  public LazyRangeMarkerFactoryImpl(@NotNull Project project, @NotNull final FileDocumentManager fileDocumentManager) {
    myProject = project;

    EditorFactory.getInstance().getEventMulticaster().addDocumentListener(new DocumentAdapter() {
      @Override
      public void beforeDocumentChange(DocumentEvent e) {
        transformRangeMarkers(e);
      }

      @Override
      public void documentChanged(DocumentEvent e) {
        transformRangeMarkers(e);
      }

      private void transformRangeMarkers(@NotNull DocumentEvent e) {
        Document document = e.getDocument();
        VirtualFile file = fileDocumentManager.getFile(document);
        if (file == null || myProject.isDisposed()) {
          return;
        }

        WeakList<LazyMarker> lazyMarkers = getMarkers(file);
        if (lazyMarkers == null) {
          return;
        }

        List<LazyMarker> markers = lazyMarkers.toStrongList();
        for (LazyMarker marker : markers) {
          if (file.equals(marker.getFile())) {
            marker.getOrCreateDelegate();
          }
        }
      }
    }, project);
  }

  static WeakList<LazyMarker> getMarkers(@NotNull VirtualFile file) {
    return file.getUserData(LazyRangeMarkerFactoryImpl.LAZY_MARKERS_KEY);
  }

  private static void addToLazyMarkersList(@NotNull LazyMarker marker, @NotNull VirtualFile file) {
    WeakList<LazyMarker> markers = getMarkers(file);

    if (markers == null) {
      markers = file.putUserDataIfAbsent(LAZY_MARKERS_KEY, new WeakList<LazyMarker>());
    }
    markers.add(marker);
  }

  private static void removeFromLazyMarkersList(@NotNull LazyMarker marker, @NotNull VirtualFile file) {
    WeakList<LazyMarker> markers = getMarkers(file);

    if (markers != null) {
      markers.remove(marker);
    }
  }

  @Override
  @NotNull
  public RangeMarker createRangeMarker(@NotNull final VirtualFile file, final int offset) {
    return ApplicationManager.getApplication().runReadAction(new Computable<RangeMarker>() {
      @Override
      public RangeMarker compute() {
        // even for already loaded document do not create range marker yet - wait until it really needed when e.g. user clicked to jump to OpenFileDescriptor
        final LazyMarker marker = new OffsetLazyMarker(file, offset);
        addToLazyMarkersList(marker, file);
        return marker;
      }
    });
  }

  @Override
  @NotNull
  public RangeMarker createRangeMarker(@NotNull final VirtualFile file, final int line, final int column, final boolean persistent) {
    return ApplicationManager.getApplication().runReadAction(new Computable<RangeMarker>() {
      @Override
      public RangeMarker compute() {
        final Document document = FileDocumentManager.getInstance().getCachedDocument(file);
        if (document != null) {
          int myTabSize = CodeStyleFacade.getInstance(myProject).getTabSize(file.getFileType());
          final int offset = calculateOffset(document, line, column, myTabSize);
          return document.createRangeMarker(offset, offset, persistent);
        }

        final LazyMarker marker = new LineColumnLazyMarker(myProject, file, line, column);
        addToLazyMarkersList(marker, file);
        return marker;
      }
    });
  }

  abstract static class LazyMarker extends UserDataHolderBase implements RangeMarker {
    protected RangeMarker myDelegate; // the real range marker which is created only when document is opened, or (this) which means it's disposed
    protected final VirtualFile myFile;
    protected final int myInitialOffset;

    private LazyMarker(@NotNull VirtualFile file, int offset) {
      myFile = file;
      myInitialOffset = offset;
    }

    boolean isDelegated() {
      return myDelegate != null;
    }

    @NotNull
    public VirtualFile getFile() {
      return myFile;
    }

    @Nullable
    final RangeMarker getOrCreateDelegate() {
      if (myDelegate == null) {
        Document document = FileDocumentManager.getInstance().getDocument(myFile);
        if (document == null) {
          return null;
        }
        myDelegate = createDelegate(myFile, document);
        removeFromLazyMarkersList(this, myFile);
      }
      return isDisposed() ? null : myDelegate;
    }

    @Nullable
    protected abstract RangeMarker createDelegate(@NotNull VirtualFile file, @NotNull Document document);

    @Override
    @NotNull
    public Document getDocument() {
      RangeMarker delegate = getOrCreateDelegate();
      if (delegate == null) {
        //noinspection ConstantConditions
        return FileDocumentManager.getInstance().getDocument(myFile);
      }
      return delegate.getDocument();
    }

    @Override
    public int getStartOffset() {
      return myDelegate == null || isDisposed() ? myInitialOffset : myDelegate.getStartOffset();
    }

    public boolean isDisposed() {
      return myDelegate == this;
    }


    @Override
    public int getEndOffset() {
      return myDelegate == null || isDisposed() ? myInitialOffset : myDelegate.getEndOffset();
    }

    @Override
    public boolean isValid() {
      RangeMarker delegate = getOrCreateDelegate();
      return delegate != null && !isDisposed() && delegate.isValid();
    }

    @Override
    public void setGreedyToLeft(boolean greedy) {
      getOrCreateDelegate().setGreedyToLeft(greedy);
    }

    @Override
    public void setGreedyToRight(boolean greedy) {
      getOrCreateDelegate().setGreedyToRight(greedy);
    }

    @Override
    public boolean isGreedyToRight() {
      return getOrCreateDelegate().isGreedyToRight();
    }

    @Override
    public boolean isGreedyToLeft() {
      return getOrCreateDelegate().isGreedyToLeft();
    }

    @Override
    public void dispose() {
      assert !isDisposed();
      RangeMarker delegate = myDelegate;
      if (delegate == null) {
        removeFromLazyMarkersList(this, myFile);
        myDelegate = this; // mark of disposed marker
      }
      else {
        delegate.dispose();
      }
    }
  }

  private static class OffsetLazyMarker extends LazyMarker {
    private OffsetLazyMarker(@NotNull VirtualFile file, int offset) {
      super(file, offset);
    }

    @Override
    public boolean isValid() {
      RangeMarker delegate = myDelegate;
      if (delegate == null) {
        Document document = FileDocumentManager.getInstance().getDocument(myFile);
        return document != null;
      }

      return super.isValid();
    }

    @Override
    @NotNull
    public RangeMarker createDelegate(@NotNull VirtualFile file, @NotNull final Document document) {
      final int offset = Math.min(myInitialOffset, document.getTextLength());
      return document.createRangeMarker(offset, offset);
    }
  }

  private static class LineColumnLazyMarker extends LazyMarker {
    private final int myLine;
    private final int myColumn;
    private final int myTabSize;

    private LineColumnLazyMarker(@NotNull Project project, @NotNull VirtualFile file, int line, int column) {
      super(file, -1);
      myLine = line;
      myColumn = column;
      myTabSize = CodeStyleFacade.getInstance(project).getTabSize(file.getFileType());
    }

    @Override
    @Nullable
    public RangeMarker createDelegate(@NotNull VirtualFile file, @NotNull Document document) {
      if (document.getTextLength() == 0 && !(myLine == 0 && myColumn == 0)) {
        return null;
      }

      int offset = calculateOffset(document, myLine, myColumn, myTabSize);
      return document.createRangeMarker(offset, offset);
    }

    @Override
    public boolean isValid() {
      RangeMarker delegate = myDelegate;
      if (delegate == null) {
        Document document = FileDocumentManager.getInstance().getDocument(myFile);
        return document != null && (document.getTextLength() != 0 || myLine == 0 && myColumn == 0);
      }

      return super.isValid();
    }

    @Override
    public int getStartOffset() {
      getOrCreateDelegate();
      return super.getStartOffset();
    }

    @Override
    public int getEndOffset() {
      getOrCreateDelegate();
      return super.getEndOffset();
    }
  }

  private static int calculateOffset(@NotNull Document document,
                                     final int line,
                                     final int column,
                                     int tabSize) {
    int offset;
    if (0 <= line && line < document.getLineCount()) {
      final int lineStart = document.getLineStartOffset(line);
      final int lineEnd = document.getLineEndOffset(line);
      final CharSequence docText = document.getCharsSequence();

      offset = lineStart;
      int col = 0;
      while (offset < lineEnd && col < column) {
        col += docText.charAt(offset) == '\t' ? tabSize : 1;
        offset++;
      }
    }
    else {
      offset = document.getTextLength();
    }
    return offset;
  }

}
