// 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.server.documents;

import java.util.List;
import java.util.Map;

import com.google.collide.dto.DocumentSelection;
import com.google.collide.dto.server.DtoServerImpls.DocumentSelectionImpl;
import com.google.collide.dto.server.DtoServerImpls.FilePositionImpl;
import com.google.collide.shared.document.anchor.Anchor;
import com.google.collide.shared.document.anchor.Anchor.RemovalStrategy;
import com.google.collide.shared.document.anchor.AnchorType;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;

/**
 * Helper that tracks the selection (cursor and base positions) for each user.
 * 
 */
public class SelectionTracker {

  private class UserSelection {
    private final String clientId;

    private VersionedDocument document;
    private String resourceId;
    private Anchor cursorAnchor;
    private Anchor baseAnchor;

    private UserSelection(String clientId) {
      this.clientId = clientId;
    }

    private synchronized void teardown() {
      removeAnchors();
    }

    private synchronized void markActive(
        final String resourceId, VersionedDocument document, DocumentSelection documentSelection) {

      if (this.document != document) {
        /*
         * The user switched files or the VersionedDocument instance swapped underneath us (in the
         * latter case, just checking the equality of FileEditSessionKeys would not be enough)
         */
        removeAnchors();

        this.resourceId = resourceId;
        this.document = document;
      }

      if (documentSelection != null) {
        // Cursor/selection has moved via explicit movement action
        moveOrCreateAnchors(document, documentSelection);
      }
    }

    private synchronized void moveOrCreateAnchors(
        VersionedDocument document, DocumentSelection documentSelection) {

      if (cursorAnchor != null) {
        document.moveAnchor(cursorAnchor, documentSelection.getCursorPosition().getLineNumber(),
            documentSelection.getCursorPosition().getColumn());
        document.moveAnchor(baseAnchor, documentSelection.getBasePosition().getLineNumber(),
            documentSelection.getBasePosition().getColumn());
      } else {
        cursorAnchor = document.addAnchor(
            CURSOR_ANCHOR_TYPE, documentSelection.getCursorPosition().getLineNumber(),
            documentSelection.getCursorPosition().getColumn());
        cursorAnchor.setRemovalStrategy(RemovalStrategy.SHIFT);

        baseAnchor = document.addAnchor(
            BASE_ANCHOR_TYPE, documentSelection.getBasePosition().getLineNumber(),
            documentSelection.getBasePosition().getColumn());
        baseAnchor.setRemovalStrategy(RemovalStrategy.SHIFT);
      }
    }

    private synchronized void removeAnchors() {
      if (cursorAnchor != null) {
        document.removeAnchor(cursorAnchor);
        cursorAnchor = null;
      }

      if (baseAnchor != null) {
        document.removeAnchor(baseAnchor);
        baseAnchor = null;
      }
    }

    @Override
    public boolean equals(Object obj) {
      return obj instanceof UserSelection && clientId.equals(((UserSelection) obj).clientId);
    }

    @Override
    public int hashCode() {
      return clientId.hashCode();
    }
  }

  private static final AnchorType CURSOR_ANCHOR_TYPE =
      AnchorType.create(SelectionTracker.class, "cursor");

  private static final AnchorType BASE_ANCHOR_TYPE =
      AnchorType.create(SelectionTracker.class, "base");

  /**
   * All active selections, keyed by the user's gaia ID.
   */
  private final Map<String, UserSelection> userSelections = Maps.newHashMap();

  @VisibleForTesting
  SelectionTracker() {
    // TODO: Listen for client disconnections to remove selections.
  }

  /**
   * Releases all resources.
   */
  public void close() {
    for (UserSelection selection : userSelections.values()) {
      selection.teardown();
    }
  }

  /**
   * Called when the selection for a user changes.
   * 
   * @param clientId the ID for the user's tab
   * @param resourceId the key for the file edit session that holds the active selection
   * @param documentSelection the position of the selection after any mutations currently being
   *        processed
   */
  public void selectionChanged(String clientId, String resourceId, VersionedDocument document,
      DocumentSelection documentSelection) {

    UserSelection selection = userSelections.get(clientId);
    if (selection == null) {
      selection = new UserSelection(clientId);
      userSelections.put(clientId, selection);
    }

    selection.markActive(resourceId, document, documentSelection);
  }

  public List<DocumentSelection> getDocumentSelections(String resourceId) {

    List<DocumentSelection> selections = Lists.newArrayList();
    for (UserSelection userSelection : userSelections.values()) {
      if (userSelection.resourceId.equals(resourceId)) {
        Anchor cursorAnchor = userSelection.cursorAnchor;
        Anchor baseAnchor = userSelection.baseAnchor;
        if (cursorAnchor != null && baseAnchor != null) {
          FilePositionImpl basePosition = FilePositionImpl.make()
              .setColumn(baseAnchor.getColumn()).setLineNumber(baseAnchor.getLineNumber());
          FilePositionImpl cursorPosition = FilePositionImpl.make().setColumn(
              cursorAnchor.getColumn()).setLineNumber(cursorAnchor.getLineNumber());
          DocumentSelectionImpl selection = DocumentSelectionImpl.make()
              .setUserId(userSelection.clientId).setBasePosition(basePosition)
              .setCursorPosition(cursorPosition);
          selections.add(selection);
        }
      }
    }

    return selections;
  }

  public void removeSelection(String clientId) {
    UserSelection selection = userSelections.remove(clientId);

    if (selection != null) {
      selection.teardown();
    }
  }
}
