// 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.util.Elements;

import com.google.collide.client.util.PathUtil;
import com.google.collide.dto.NodeConflictDto.ConflictedPath;
import com.google.collide.dto.TreeNodeInfo;
import com.google.collide.json.shared.JsonArray;
import com.google.collide.mvp.CompositeView;
import com.google.collide.mvp.UiComponent;
import com.google.collide.shared.util.JsonCollections;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.resources.client.ClientBundle;
import com.google.gwt.resources.client.CssResource;
import com.google.gwt.resources.client.DataResource;

import elemental.css.CSSStyleDeclaration;
import elemental.dom.Element;

/**
 * A trail of "breadcrumbs" representing the current file path and the current
 * code scope where the cursor is placed. If the cursor is inside a function
 * foo() {...} inside class bar, with the current file being
 * /www/widgets/widget.js, then the breadcrumb trail would look like: www >
 * widgets > widget.js > bar > foo.
 */
public class WorkspaceLocationBreadcrumbs extends UiComponent<WorkspaceLocationBreadcrumbs.View> {

  /**
   * BaseCss resources for breadcrumbs.
   */
  public interface Css extends CssResource {
    String breadcrumbBar();

    String breadcrumbWrap();

    String breadcrumbSlash();

    String breadcrumb();

    String start();

    String hide();

    /** Icons */
    String directory();

    String file();

    String cls();

    String function();

    String field();
  }

  /**
   * Client bundle for breadcrumbs.
   */
  /*
   * TODO: Figure out a way to break the css reliance on data resources
   * then use the folder() and file() from base.
   */
  public interface Resources extends ClientBundle {
    @Source("WorkspaceLocationBreadcrumbs.css")
    Css workspaceLocationBreadcrumbsCss();

    @Source("collide/client/common/folder_breadcrumb.png")
    DataResource directoryImg();

    @Source("collide/client/common/file_breadcrumb.png")
    DataResource fileImg();

    @Source("path_slash.png")
    DataResource pathSlashImg();
  }

  private static enum PathType {
    FOLDER, FILE, CLASS, FUNCTION, FIELD
  }

  /**
   * A class representing a single breadcrumb.
   */
  static class Breadcrumb extends UiComponent<Breadcrumb.View> {

    /**
     * The view for a single breadcrumb.
     */
    class View extends CompositeView<Void> {

      private Element breadcrumb;
      private final Css css;

      View(Resources res, String name, String iconClass) {
        css = res.workspaceLocationBreadcrumbsCss();
        Element breadcrumbSlash = Elements.createSpanElement(css.breadcrumbSlash());
        breadcrumbSlash.setTextContent("/");

        breadcrumb = Elements.createSpanElement(css.breadcrumb());
        breadcrumb.addClassName(iconClass);
        breadcrumb.setTextContent(name);

        Element breadcrumbWrap = Elements.createSpanElement(css.breadcrumbWrap());
        breadcrumbWrap.appendChild(breadcrumbSlash);
        breadcrumbWrap.appendChild(breadcrumb);

        // Start hidden
        breadcrumb.addClassName(css.start());
        setElement(breadcrumbWrap);
      }

      /**
       * Show by fading+sliding in from the left.
       */
      void show(int startDelay, int duration) {
        CSSStyleDeclaration style = breadcrumb.getStyle();
        // TODO: extract constants
        style.setProperty("-webkit-transition-duration", duration + "ms");
        style.setProperty("-webkit-transition-delay", startDelay + "ms");
        breadcrumb.removeClassName(css.start());
      }

      /**
       * Hide by fading+sliding out to the left.
       */
      void hide(int startDelay) {
        CSSStyleDeclaration style = breadcrumb.getStyle();
        style.setProperty("-webkit-transition-delay", startDelay + "ms");
        getElement().addClassName(css.hide());
      }

      void detach() {
        getElement().removeFromParent();
      }
    }

    private String name;
    private String iconClass;

    Breadcrumb(Resources res, String name, String iconClass) {
      View view = new View(res, name, iconClass);
      setView(view);
      this.name = name;
      this.iconClass = iconClass;
    }

    @Override
    public boolean equals(Object other) {
      if (other == this) {
        return true;
      }
      if (!(other instanceof Breadcrumb)) {
        return false;
      }
      Breadcrumb crumb = (Breadcrumb) other;
      return crumb.name.equals(name) && crumb.iconClass.equals(iconClass);
    }

    @Override
    public int hashCode() {
      int result = 17;
      result = 37 * result + name.hashCode();
      result = 37 * result + iconClass.hashCode();
      return result;
    }
  }

  /**
   * Number of milliseconds to delay between each breadcrumb being animated, to
   * create a path buildup/teardown effect.
   */
  private static final int DELAY_SHOW_TIME_MIN = 100;
  private static final int DELAY_SHOW_TIME_MAX = 200;
  private static final int DELAY_HIDE_TIME = 150;
  private PathUtil currentFilePath = null;
  private JsonArray<Breadcrumb> currentPathCrumbs = JsonCollections.createArray();

  /**
   * The view for the breadcrumbs.
   */
  public static class View extends CompositeView<Void> {

    private final Css css;
    private final Resources res;

    View(Resources res) {
      this.res = res;
      this.css = res.workspaceLocationBreadcrumbsCss();
      Element breadcrumbBar = Elements.createDivElement(css.breadcrumbBar());
      setElement(breadcrumbBar);
    }

    public Breadcrumb createBreadcrumb(String name, PathType type) {
      String iconStyle = "";
      switch (type) {
        case FOLDER:
          iconStyle = css.directory();
          break;
        case FILE:
          iconStyle = css.file();
          break;
        case CLASS:
          iconStyle = css.cls();
          break;
        case FUNCTION:
          iconStyle = css.function();
          break;
        case FIELD:
          iconStyle = css.field();
          break;
      }
      return new Breadcrumb(res, name, iconStyle);
    }

    /**
     * First fade out all elements in remove, then fade in elements in add.
     */
    void updateElements(
        final JsonArray<Breadcrumb.View> remove, final JsonArray<Breadcrumb.View> add) {
      for (int i = remove.size() - 1; i >= 0; i--) {
        remove.get(i).hide(0);
      }
      // Detach elements after animation finishes, then start the add animation
      Scheduler.get().scheduleFixedPeriod(new Scheduler.RepeatingCommand() {
        @Override
        public boolean execute() {
          for (int i = 0, n = remove.size(); i < n; i++) {
            remove.get(i).detach();
          }
          int n = add.size();
          if (n > 0) {
            for (int i = 0; i < n; i++) {
              getElement().appendChild(add.get(i).getElement());
            }
            // Trigger a browser layout so CSS3 transitions fire
            add.get(0).getElement().getClientWidth();
            int showDuration = Math.min(n * DELAY_SHOW_TIME_MIN, DELAY_SHOW_TIME_MAX) / n;
            for (int i = 0; i < n; i++) {
              add.get(i).show(showDuration * i, showDuration);
            }
          }
          return false;
        }
      }, DELAY_HIDE_TIME);
    }
  }

  /**
   * Calculate the difference between the currently displayed path and the new
   * file+scope paths and animate the change.
   */
  void changeBreadcrumbPath(JsonArray<Breadcrumb> newPath) {
    // find path difference
    JsonArray<Breadcrumb.View> remove = JsonCollections.createArray();
    JsonArray<Breadcrumb.View> add = JsonCollections.createArray();
    /*
     * Walk from the existing beginning to the end, and once a difference is
     * found, mark the rest of the existing path to be removed and the rest of
     * the new path to be added.
     */
    Breadcrumb newBreadcrumb;
    Breadcrumb curBreadcrumb;
    boolean isMatchingPath = true;
    int i = 0;
    for (int n = currentPathCrumbs.size(), m = newPath.size(); i < n || i < m; i++) {
      if (i < n && i < m) {
        // Check for path conflict, add and remove
        curBreadcrumb = currentPathCrumbs.get(i);
        newBreadcrumb = newPath.get(i);
        if (!isMatchingPath || !curBreadcrumb.equals(newBreadcrumb)) {
          isMatchingPath = false;
          remove.add(curBreadcrumb.getView());
          add.add(newBreadcrumb.getView());
        } else {
          // Equal, update new list with old Breadcrumb
          newPath.set(i, curBreadcrumb);
        }
      } else if (i >= n) {
        // No current path here, add new path
        add.add(newPath.get(i).getView());
      } else if (i >= m) {
        // No new path here, remove current path
        remove.add(currentPathCrumbs.get(i).getView());
      }
    }
    currentPathCrumbs = newPath;
    getView().updateElements(remove, add);
  }

  /**
   * Clears the current breadcrumb path.
   */
  public void clearPath() {
    currentFilePath = null;
    changeBreadcrumbPath(JsonCollections.<Breadcrumb>createArray());
  }

  /**
   * Returns the current breadcrumb path.
   */
  public PathUtil getPath() {
    return currentFilePath;
  }

  /**
   * Sets the path that these breadcrumbs display.
   */
  public void setPath(PathUtil newPath) {
    // TODO: Uncomment when onCursorLocationChanged is implemented.
    // editor.getSelection().getCursorListenerRegistrar().add(cursorListener);
    JsonArray<Breadcrumb> newFilePath = JsonCollections.createArray();
    for (int i = 0, n = newPath.getPathComponentsCount(); i < n; i++) {
      if (i == n - 1) {
        newFilePath.add(getView().createBreadcrumb(newPath.getPathComponent(i), PathType.FILE));
      } else {
        newFilePath.add(getView().createBreadcrumb(newPath.getPathComponent(i), PathType.FOLDER));
      }
    }
    currentFilePath = newPath;
    changeBreadcrumbPath(newFilePath);
  }

  /**
   * Sets the path and type that these breadcrumbs display.
   */
  public void setPath(ConflictedPath conflictedPath) {
    PathUtil newPath = new PathUtil(conflictedPath.getPath());
    JsonArray<Breadcrumb> newFilePath = JsonCollections.createArray();
    for (int i = 0, n = newPath.getPathComponentsCount(); i < n; i++) {
      if (i == n - 1 && conflictedPath.getNodeType() == TreeNodeInfo.FILE_TYPE) {
        newFilePath.add(getView().createBreadcrumb(newPath.getPathComponent(i), PathType.FILE));
      } else {
        newFilePath.add(getView().createBreadcrumb(newPath.getPathComponent(i), PathType.FOLDER));
      }
    }
    currentFilePath = newPath;
    changeBreadcrumbPath(newFilePath);
  }

  /*
   * TODO: Implement to show the current scope of the cursor.
   */
  public void onCursorLocationChanged(int lineNumber, int column) {
    // TODO: Do this async to avoid cursor lag.
    JsonArray<Breadcrumb> newPath = currentPathCrumbs.copy();
    // JsoArray<String> scope = autocompleter.getLocationScope(lineNumber,
    // column);
    newPath.add(getView().createBreadcrumb("Line " + lineNumber, PathType.CLASS));
    newPath.add(getView().createBreadcrumb("Column " + column, PathType.FUNCTION));
    changeBreadcrumbPath(newPath);
  }
}
