// 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.client.code;

import collide.client.filetree.FileTreeModel;
import collide.client.filetree.FileTreeModel.NodeRequestCallback;
import collide.client.filetree.FileTreeNode;
import collide.client.filetree.FileTreeUiController;
import collide.client.treeview.TreeNodeElement;
import collide.client.util.Elements;

import com.google.collide.client.document.DocumentManager;
import com.google.collide.client.document.DocumentManager.GetDocumentCallback;
import com.google.collide.client.document.DocumentMetadata;
import com.google.collide.client.history.RootPlace;
import com.google.collide.client.ui.panel.MultiPanel;
import com.google.collide.client.util.PathUtil;
import com.google.collide.client.workspace.WorkspacePlace;
import com.google.collide.dto.FileContents;
import com.google.collide.dto.ServerError.FailureReason;
import com.google.collide.json.client.JsoArray;
import com.google.collide.shared.document.Document;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.google.gwt.event.shared.EventHandler;
import com.google.gwt.event.shared.GwtEvent;

/**
 * Handler for file selection that drives file contents and the file tree selection.
 *
 */
public class FileSelectionController implements GetDocumentCallback {

  /**
   * Event that broadcasts that a file's contents have been received over the network and we have
   * determined if it is editable.
   *
   */
  public static class FileOpenedEvent extends GwtEvent<FileOpenedEvent.Handler> {

    public interface Handler extends EventHandler {
      public void onFileOpened(boolean isEditable, PathUtil filePath);
    }

    public static final Type<Handler> TYPE = new Type<Handler>();
    private final boolean isFileEditable;
    private final PathUtil filePath;

    public FileOpenedEvent(boolean isEditable, PathUtil filePath) {
      this.isFileEditable = isEditable;
      this.filePath = filePath;
    }

    @Override
    protected void dispatch(Handler handler) {
      handler.onFileOpened(isFileEditable, filePath);
    }

    public boolean isEditable() {
      return isFileEditable;
    }

    public PathUtil getFilePath() {
      return filePath;
    }

    @Override
    public com.google.gwt.event.shared.GwtEvent.Type<Handler> getAssociatedType() {
      return TYPE;
    }
  }

  private FileSelectedPlace.NavigationEvent mostRecentNavigationEvent;
  private boolean isSelectedFileEditable = false;
  private final FileTreeUiController treeUiController;
  private final FileTreeModel fileTreeModel;
  private final MultiPanel<?,?> contentArea;
  private final EditorBundle editorBundle;
  private final UneditableDisplay uneditableDisplay;
  private final DocumentManager documentManager;

  public FileSelectionController(DocumentManager documentManager,
      EditorBundle editorBundle,
      UneditableDisplay uneditableDisplay,
      FileTreeModel fileTreeModel,
      FileTreeUiController treeUiController,
      MultiPanel<?,?> masterPanel) {
    this.documentManager = documentManager;
    this.editorBundle = editorBundle;
    this.uneditableDisplay = uneditableDisplay;
    this.fileTreeModel = fileTreeModel;
    this.treeUiController = treeUiController;
    this.contentArea = masterPanel;
  }

  /**
   * Deselects the currently selected file, if one is selected.
   */
  public void deselectFile() {
    mostRecentNavigationEvent = null;
    treeUiController.clearSelectedNodes();
  }

  public void selectFile(FileSelectedPlace.NavigationEvent navigationEvent) {
    // The root is a special case no-op.
    if (!navigationEvent.getPath().equals(PathUtil.WORKSPACE_ROOT)) {
      doSelectTreeNode(navigationEvent);
    }
  }

  public boolean isSelectedFileEditable() {
    return isSelectedFileEditable;
  }

  FileTreeModel getFileTreeModel() {
    return fileTreeModel;
  }

  private void doSelectTreeNode(final FileSelectedPlace.NavigationEvent navigationEvent) {
    mostRecentNavigationEvent = navigationEvent;

    fileTreeModel.requestWorkspaceNode(navigationEvent.getPath(), new NodeRequestCallback() {

      @Override
      public void onNodeAvailable(FileTreeNode node) {

        // If we have since navigated away, exit early and don't select the
        // file.
        if (mostRecentNavigationEvent != navigationEvent
            || !mostRecentNavigationEvent.isActiveLeaf()) {
          return;
        }

        // Expand the tree to reveal the node if it happens to be hidden (in the
        // case of deep linking)
        TreeNodeElement<FileTreeNode> renderedElement = node.getRenderedTreeNode();
        if (renderedElement == null
            || !renderedElement.isActive(treeUiController.getTree().getResources().treeCss())) {

          // Select the node without dispatching the node selection action.
          treeUiController.autoExpandAndSelectNode(node, false);
        }
      }

      @Override
      public void onNodeUnavailable() {
        // This can happen if a history event is dispatched for a path not
        // found in our file tree.

        // TODO: Throw up some UI to show that no such file exists.
        // For now simply ignore the selection.
      }

      @Override
      public void onError(FailureReason reason) {
        // Already logged by the FileTreeModel.
      }
    });

    documentManager.getDocument(navigationEvent.getPath(), this);
  }

  @Override
  public void onDocumentReceived(Document document) {
    PathUtil path = DocumentMetadata.getPath(document);

    if (mostRecentNavigationEvent == null || !mostRecentNavigationEvent.isActiveLeaf()
        || !path.equals(mostRecentNavigationEvent.getPath())) {
      // User selected another file or navigated away since this request, ignore
      return;
    }

    openDocument(document, path);
    WorkspacePlace.PLACE.fireEvent(new FileOpenedEvent(true, path));
  }

  private void openDocument(final Document document, PathUtil path) {
    isSelectedFileEditable = true;

    contentArea.setContent(editorBundle);

    // Note that we dont use the name from the incoming FileContents. They
    // should generally be the same. But in the case of a rename or a move, they
    // might not be. Those changes get propagated separately, so we stick with
    // the client's view of the world wrt to naming.
    editorBundle.setDocument(document, path, DocumentMetadata.getFileEditSessionKey(document));

    if (mostRecentNavigationEvent.getLineNo()
        != FileSelectedPlace.NavigationEvent.IGNORE_LINE_NUMBER) {
      /*
       * TODO: This scheduled deferred is so we set scroll AFTER the selection
       * restorer. After demo, I'll create a better API on editor for components that want to set
       * initial selection/scroll.
       */
      final FileSelectedPlace.NavigationEvent savedNavigationEvent = mostRecentNavigationEvent;
      Scheduler.get().scheduleDeferred(new ScheduledCommand() {
        @Override
        public void execute() {
          if (mostRecentNavigationEvent == savedNavigationEvent) {
            editorBundle.getEditor().scrollTo(mostRecentNavigationEvent.getLineNo(),
                Math.max(0, mostRecentNavigationEvent.getColumn()));
          }
        }
      });
    }

    // Set the tab title to the current open file
    Elements.setCollideTitle(path.getBaseName());

    editorBundle.getEditor().getFocusManager().focus();

    // Save the list of open documents.
    // TODO: Send a list of files when we support tabs.
    JsoArray<String> openFiles = JsoArray.create();
    openFiles.add(path.getPathString());
  }

  @Override
  public void onUneditableFileContentsReceived(FileContents uneditableFile) {
    showDisplayOnly(uneditableFile);
    WorkspacePlace.PLACE.fireEvent(
        new FileOpenedEvent(false, new PathUtil(uneditableFile.getPath())));
  }

  /**
   * Changes the display for an uneditable file.
   */
  private void showDisplayOnly(FileContents uneditableFile) {
    PathUtil filePath = new PathUtil(uneditableFile.getPath());
    if (!filePath.equals(mostRecentNavigationEvent.getPath())) {
      // User selected another file since this request, ignore
      return;
    }

    isSelectedFileEditable = false;

    // Set the tab title to the current open file
    Elements.setCollideTitle(filePath.getBaseName());

    contentArea.setContent(uneditableDisplay);
    uneditableDisplay.displayUneditableFileContents(uneditableFile);
    editorBundle.getBreadcrumbs().setPath(filePath);
  }

  @Override
  public void onFileNotFoundReceived() {
    // TODO: pretty file not found message
    RootPlace.PLACE.fireChildPlaceNavigation(
        WorkspacePlace.PLACE.createNavigationEvent());
  }
}
