/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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 org.apache.zeppelin.notebook;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.LinkedHashMap;
import java.util.Map;

/**
 * Folder view of notes of Notebook.
 * FolderView allows you to see notes from perspective of folders.
 */
public class FolderView implements NoteNameListener, FolderListener {
  // key: folderId
  private final Map<String, Folder> folders = new LinkedHashMap<>();
  // key: a note, value: a folder where the note belongs to
  private final Map<Note, Folder> index = new LinkedHashMap<>();

  private static final Logger logger = LoggerFactory.getLogger(FolderView.class);

  public Folder getFolder(String folderId) {
    String normalizedFolderId = Folder.normalizeFolderId(folderId);
    return folders.get(normalizedFolderId);
  }

  /**
   * Rename folder of which id is folderId to newFolderId
   *
   * @param oldFolderId folderId to rename
   * @param newFolderId newFolderId
   * @return `null` if folder not exists, else old Folder
   * in order to know which notes and child folders are renamed
   */
  public Folder renameFolder(String oldFolderId, String newFolderId) {
    String normOldFolderId = Folder.normalizeFolderId(oldFolderId);
    String normNewFolderId = Folder.normalizeFolderId(newFolderId);

    if (!hasFolder(normOldFolderId))
      return null;

    if (oldFolderId.equals(Folder.ROOT_FOLDER_ID))  // cannot rename the root folder
      return null;

    // check whether oldFolderId and newFolderId are same or not
    if (normOldFolderId.equals(normNewFolderId))
      return getFolder(normOldFolderId);

    logger.info("Rename {} to {}", normOldFolderId, normNewFolderId);

    Folder oldFolder = getFolder(normOldFolderId);
    removeFolder(oldFolderId);

    oldFolder.rename(normNewFolderId);

    return oldFolder;
  }

  public Folder getFolderOf(Note note) {
    return index.get(note);
  }

  public void putNote(Note note) {
    if (note.isNameEmpty()) {
      return;
    }

    String folderId = note.getFolderId();

    Folder folder = getOrCreateFolder(folderId);
    folder.addNote(note);

    synchronized (index) {
      index.put(note, folder);
    }
  }

  private Folder getOrCreateFolder(String folderId) {
    if (folders.containsKey(folderId))
      return folders.get(folderId);

    return createFolder(folderId);
  }

  private Folder createFolder(String folderId) {
    folderId = Folder.normalizeFolderId(folderId);

    Folder newFolder = new Folder(folderId);
    newFolder.addFolderListener(this);

    logger.info("Create folder {}", folderId);

    synchronized (folders) {
      folders.put(folderId, newFolder);
    }

    Folder parentFolder = getOrCreateFolder(newFolder.getParentFolderId());

    newFolder.setParent(parentFolder);
    parentFolder.addChild(newFolder);

    return newFolder;
  }

  private void removeFolder(String folderId) {
    Folder removedFolder;

    synchronized (folders) {
      removedFolder = folders.remove(folderId);
    }

    if (removedFolder != null) {
      logger.info("Remove folder {}", folderId);
      Folder parent = removedFolder.getParent();
      parent.removeChild(folderId);
      removeFolderIfEmpty(parent.getId());
    }
  }

  private void removeFolderIfEmpty(String folderId) {
    if (!hasFolder(folderId))
      return;

    Folder folder = getFolder(folderId);
    if (folder.countNotes() == 0 && !folder.hasChild()) {
      logger.info("Folder {} is empty", folder.getId());
      removeFolder(folderId);
    }
  }

  public void removeNote(Note note) {
    if (!index.containsKey(note)) {
      return;
    }

    Folder folder = index.get(note);
    folder.removeNote(note);

    removeFolderIfEmpty(folder.getId());

    synchronized (index) {
      index.remove(note);
    }
  }

  public void clear() {
    synchronized (folders) {
      folders.clear();
    }
    synchronized (index) {
      index.clear();
    }
  }

  public boolean hasFolder(String folderId) {
    return getFolder(folderId) != null;
  }

  public boolean hasNote(Note note) {
    return index.containsKey(note);
  }

  public int countFolders() {
    return folders.size();
  }

  public int countNotes() {
    int count = 0;

    for (Folder folder : folders.values()) {
      count += folder.countNotes();
    }

    return count;
  }

  /**
   * Fired after a note's setName() run.
   * When the note's name changed, FolderView should check if the note is in the right folder.
   *
   * @param note
   * @param oldName
   */
  @Override
  public void onNoteNameChanged(Note note, String oldName) {
    if (note.isNameEmpty()) {
      return;
    }
    logger.info("Note name changed: {} -> {}", oldName, note.getName());
    // New note
    if (!index.containsKey(note)) {
      putNote(note);
    }
    // Existing note
    else {
      // If the note is in the right place, just return
      Folder folder = index.get(note);
      if (folder.getId().equals(note.getFolderId())) {
        return;
      }
      // The note's folder is changed!
      removeNote(note);
      putNote(note);
    }
  }

  @Override
  public void onFolderRenamed(Folder folder, String oldFolderId) {
    if (getFolder(folder.getId()) == folder)  // the folder is at the right place
      return;
    logger.info("folder renamed: {} -> {}", oldFolderId, folder.getId());

    if (getFolder(oldFolderId) == folder)
      folders.remove(oldFolderId);

    Folder newFolder = getOrCreateFolder(folder.getId());
    newFolder.merge(folder);

    for (Note note : folder.getNotes()) {
      index.put(note, newFolder);
    }
  }
}
