/*******************************************************************************
 * Copyright 2011 Google Inc. All Rights Reserved.
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * 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.gwt.eclipse.core.clientbundle.ui;

import com.google.gdt.eclipse.core.JavaUtilities;
import com.google.gdt.eclipse.core.StatusUtilities;
import com.google.gdt.eclipse.core.java.JavaModelSearch;
import com.google.gdt.eclipse.core.ui.JavaProjectSelectionDialog;
import com.google.gdt.eclipse.core.ui.JavaTypeCompletionProcessorWithAutoActivation;
import com.google.gwt.eclipse.core.GWTPlugin;
import com.google.gwt.eclipse.core.GWTPluginLog;
import com.google.gwt.eclipse.core.clientbundle.ClientBundleResource;
import com.google.gwt.eclipse.core.clientbundle.ClientBundleUtilities;
import com.google.gwt.eclipse.core.nature.GWTNature;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IPackageFragment;
import org.eclipse.jdt.core.IPackageFragmentRoot;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.search.IJavaSearchConstants;
import org.eclipse.jdt.core.search.IJavaSearchScope;
import org.eclipse.jdt.core.search.SearchEngine;
import org.eclipse.jdt.internal.ui.dialogs.FilteredTypesSelectionDialog;
import org.eclipse.jdt.internal.ui.dialogs.StatusUtil;
import org.eclipse.jdt.internal.ui.dialogs.TextFieldNavigationHandler;
import org.eclipse.jdt.internal.ui.refactoring.contentassist.ControlContentAssistHelper;
import org.eclipse.jdt.internal.ui.refactoring.contentassist.JavaTypeCompletionProcessor;
import org.eclipse.jdt.internal.ui.wizards.dialogfields.DialogField;
import org.eclipse.jdt.internal.ui.wizards.dialogfields.IDialogFieldListener;
import org.eclipse.jdt.internal.ui.wizards.dialogfields.IStringButtonAdapter;
import org.eclipse.jdt.internal.ui.wizards.dialogfields.LayoutUtil;
import org.eclipse.jdt.internal.ui.wizards.dialogfields.StringButtonDialogField;
import org.eclipse.jface.dialogs.StatusDialog;
import org.eclipse.jface.window.Window;
import org.eclipse.swt.SWT;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Text;
import org.eclipse.ui.PlatformUI;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * Dialog for adding resources to an existing ClientBundle.
 */
@SuppressWarnings("restriction")
public class AddResourcesToClientBundleDialog extends StatusDialog {

  /**
   * General-purpose event handler for dialog fields.
   */
  private class FieldAdapter implements IStringButtonAdapter,
      IDialogFieldListener,
      BundledResourcesSelectionBlock.IResourcesChangeListener {
    public void changeControlPressed(DialogField field) {
      if (field == projectField) {
        IJavaProject selectedProject = JavaProjectSelectionDialog.chooseProject(
            getShell(), getJavaProject(),
            Collections.singleton(GWTNature.NATURE_ID));
        if (selectedProject != null) {
          projectField.setText(selectedProject.getElementName());
        }
      } else if (field == clientBundleTypeField) {
        IType type = chooseClientBundleType();
        if (type != null) {
          clientBundleTypeField.setText(type.getFullyQualifiedName('.'));
        }
      }
    }

    public void dialogFieldChanged(DialogField field) {
      fieldChanged();
    }

    public void onResourcesChanged() {
      fieldChanged();
    }
  }

  private final BundledResourcesSelectionBlock bundledResourcesBlock;

  private IType clientBundleType;

  private StringButtonDialogField clientBundleTypeField;

  private final FieldAdapter fieldAdapter = new FieldAdapter();

  private IFile[] files;

  private IProject project;

  private StringButtonDialogField projectField;

  private JavaTypeCompletionProcessor resourceTypeCompletionProcessor;

  public AddResourcesToClientBundleDialog(Shell parent, IProject project,
      IType clientBundleType, IFile[] files) {
    super(parent);
    this.project = project;
    this.clientBundleType = clientBundleType;
    this.files = files;
    this.bundledResourcesBlock = new BundledResourcesSelectionBlock(
        "Bundled resources:", fieldAdapter);

    setTitle("Add Resources to ClientBundle");
    setHelpAvailable(false);
    setShellStyle(getShellStyle() | SWT.RESIZE);
  }

  @Override
  public void create() {
    super.create();

    /*
     * If we had errors on any of the initially-populated fields, we want the
     * dialog to appear with an error. However, because StatusDialog explicitly
     * suppress errors from showing when the dialog is first loaded, we have to
     * override create() and call validateFields() again.
     */
    validateFields();
  }

  public IType getClientBundleType() {
    return clientBundleType;
  }

  public List<ClientBundleResource> getResources() {
    return bundledResourcesBlock.getResources();
  }

  @Override
  protected Control createContents(Composite parent) {
    Control contents = super.createContents(parent);

    initializeControls();
    addEventHandlers();
    fieldChanged();

    setInitialFocus(parent);

    return contents;
  }

  @Override
  protected Control createDialogArea(Composite parent) {
    Composite container = (Composite) super.createDialogArea(parent);
    final GridLayout gridLayout = new GridLayout();
    int nColumns = 3;
    gridLayout.numColumns = nColumns;
    gridLayout.marginHeight = 8;
    gridLayout.marginWidth = 8;
    container.setLayout(gridLayout);

    createProjectControls(container, nColumns);
    createClientBundleTypeControls(container, nColumns);
    createBundledResourcesControls(container, nColumns);

    return container;
  }

  private void addEventHandlers() {
    projectField.setDialogFieldListener(fieldAdapter);
    clientBundleTypeField.setDialogFieldListener(fieldAdapter);
  }

  private IType chooseClientBundleType() {
    try {
      // Create a search scope for finding ClientBundle subtypes
      IJavaSearchScope scope = SearchEngine.createHierarchyScope(ClientBundleUtilities.findClientBundleType(getJavaProject()));

      // Configure the type selection dialog
      FilteredTypesSelectionDialog dialog = new FilteredTypesSelectionDialog(
          getShell(), false, PlatformUI.getWorkbench().getProgressService(),
          scope, IJavaSearchConstants.INTERFACE);
      dialog.setTitle("ClientBundle Type Selection");
      dialog.setMessage("Choose a type:");

      if (dialog.open() == Window.OK) {
        return (IType) dialog.getFirstResult();
      }
    } catch (JavaModelException e) {
      GWTPluginLog.logError(e);
    }

    return null;
  }

  private void clientBundleTypeChanged() {
    // Ensure the controls' enablement is correct
    updateControls();

    // Notify the bundled resources block about the new ClientBundle type
    if (clientBundleType != null) {
      IPackageFragment pckgFragment = clientBundleType.getPackageFragment();
      bundledResourcesBlock.setPackage(pckgFragment);
      // Since we already have an existing ClientBundle type, it is the only
      // type we need to search for conflicting method names (unlike in the New
      // ClientBundle wizard, where the user can choose to extend additional
      // arbitrary interfaces in addition to ClientBundle).
      bundledResourcesBlock.setExtendedInterfaces(new String[] {clientBundleType.getFullyQualifiedName('.')});
    } else {
      bundledResourcesBlock.setPackage(null);
      bundledResourcesBlock.setExtendedInterfaces(new String[0]);
    }
  }

  private void createBundledResourcesControls(Composite parent, int columns) {
    bundledResourcesBlock.doFillIntoGrid(parent, columns);
  }

  private void createClientBundleTypeControls(Composite parent, int nColumns) {
    clientBundleTypeField = new StringButtonDialogField(fieldAdapter);
    clientBundleTypeField.setLabelText("ClientBundle:");
    clientBundleTypeField.setButtonLabel("Browse...");
    clientBundleTypeField.doFillIntoGrid(parent, nColumns);
    Text text = clientBundleTypeField.getTextControl(null);
    LayoutUtil.setWidthHint(text, getMaxFieldWidth());

    // Set up auto-completion
    resourceTypeCompletionProcessor = new JavaTypeCompletionProcessorWithAutoActivation();
    ControlContentAssistHelper.createTextContentAssistant(text,
        resourceTypeCompletionProcessor);
    TextFieldNavigationHandler.install(text);
  }

  private void createProjectControls(Composite parent, int nColumns) {
    projectField = new StringButtonDialogField(fieldAdapter);
    projectField.setLabelText("Project:");
    projectField.setButtonLabel("Browse...");
    projectField.doFillIntoGrid(parent, nColumns);
    Text text = projectField.getTextControl(null);
    LayoutUtil.setWidthHint(text, getMaxFieldWidth());
    LayoutUtil.setHorizontalGrabbing(projectField.getTextControl(null));
  }

  private void fieldChanged() {
    validateFields();
    updateControls();
  }

  private IJavaProject getJavaProject() {
    return JavaCore.create(project);
  }

  private int getMaxFieldWidth() {
    return convertWidthInCharsToPixels(60);
  }

  private void initBundledResources() {
    assert (getJavaProject() != null);

    List<ClientBundleResource> resources = new ArrayList<ClientBundleResource>();

    // Try to wrap each file from initial selection as a ClientBundle resource
    for (IFile file : files) {
      ClientBundleResource resource = ClientBundleResource.createFromFile(
          getJavaProject(), file);
      if (resource != null) {
        resources.add(resource);
      }
    }
    bundledResourcesBlock.setResources(resources);
  }

  private void initializeControls() {
    if (project != null) {
      projectField.setText(project.getName());
      projectChanged();

      IJavaProject javaProject = getJavaProject();
      if (javaProject != null) {
        if (clientBundleType != null) {
          clientBundleTypeField.setText(clientBundleType.getFullyQualifiedName('.'));
          clientBundleTypeChanged();
        }

        if (files != null) {
          initBundledResources();
        }
      }
    }
  }

  private void projectChanged() {
    // Ensure the controls' enablement is correct
    updateControls();

    // Notify the bundled resources block about the new project
    bundledResourcesBlock.setJavaProject(getJavaProject());

    // Notify the ClientBundle type field auto-completion processor
    setPackageFragmentForTypeCompletion();
  }

  private void setInitialFocus(Composite parent) {
    if (project == null) {
      projectField.postSetFocusOnDialogField(parent.getDisplay());
    } else if (clientBundleType == null) {
      clientBundleTypeField.postSetFocusOnDialogField(parent.getDisplay());
    }
  }

  private void setPackageFragmentForTypeCompletion() {
    IPackageFragment pckgFragment = null;
    IJavaProject javaProject = getJavaProject();
    if (javaProject != null) {
      try {
        IPackageFragmentRoot[] srcRoots = javaProject.getPackageFragmentRoots();
        if (srcRoots.length > 0) {
          // Just use the default package of the first source root we find
          pckgFragment = srcRoots[0].getPackageFragment("");
        }
      } catch (JavaModelException e) {
        GWTPluginLog.logError(e);
      }
    }
    resourceTypeCompletionProcessor.setPackageFragment(pckgFragment);
  }

  private void updateControls() {
    boolean enableClientBundleTypeField = (project != null);
    boolean enableBundledResourcesBlock = (enableClientBundleTypeField && clientBundleType != null);

    if (enableClientBundleTypeField != clientBundleTypeField.isEnabled()) {
      clientBundleTypeField.setEnabled(enableClientBundleTypeField);
    }

    if (enableBundledResourcesBlock != bundledResourcesBlock.isEnabled()) {
      bundledResourcesBlock.setEnabled(enableBundledResourcesBlock);
    }
  }

  private IStatus validateBundledResources() {
    IStatus blockStatus = bundledResourcesBlock.getStatus();

    // If the block reports an error, return that
    if (!blockStatus.isOK()) {
      return blockStatus;
    }

    // Must add at least one resource to the ClientBundle
    if (bundledResourcesBlock.getResources().size() == 0) {
      // Empty message will disable the OK button, but not display an error
      return StatusUtilities.newErrorStatus("", GWTPlugin.PLUGIN_ID);
    }

    return StatusUtilities.OK_STATUS;
  }

  private IStatus validateClientBundleType() {
    IType oldClientBundleType = clientBundleType;
    clientBundleType = null;

    if (getJavaProject() == null) {
      // Bail out if we don't have a valid Java project; we can return OK since
      // the error on the project will take precedence.
      return StatusUtilities.OK_STATUS;
    }

    String clientBundleTypeName = clientBundleTypeField.getText().trim();
    if (clientBundleTypeName.length() == 0) {
      // Empty message will disable the OK button, but not display an error
      return StatusUtilities.newErrorStatus("", GWTPlugin.PLUGIN_ID);
    }

    IType enteredClientBundleType = JavaModelSearch.findType(getJavaProject(),
        clientBundleTypeName);
    if (enteredClientBundleType == null) {
      return StatusUtilities.newErrorStatus(
          "ClientBundle type does not exist", GWTPlugin.PLUGIN_ID);
    }

    try {
      if (!ClientBundleUtilities.isClientBundle(getJavaProject(),
          enteredClientBundleType)) {
        return StatusUtilities.newErrorStatus("ClientBundle type must extend "
            + ClientBundleUtilities.CLIENT_BUNDLE_TYPE_NAME,
            GWTPlugin.PLUGIN_ID);
      }
    } catch (JavaModelException e) {
      GWTPluginLog.logError(e);
      return StatusUtilities.newErrorStatus(
          "Error while calculating super types.  See Eclipse log for details.",
          GWTPlugin.PLUGIN_ID);
    }

    // ClientBundle type is valid, so set it
    this.clientBundleType = enteredClientBundleType;

    // Notify others if the ClientBundle type has changed
    if (!JavaUtilities.equalsWithNullCheck(oldClientBundleType,
        enteredClientBundleType)) {
      clientBundleTypeChanged();
    }

    return StatusUtilities.OK_STATUS;
  }

  private void validateFields() {
    IStatus projectStatus = validateProject();
    IStatus clientBundleTypeStatus = validateClientBundleType();
    IStatus bundledResourcesStatus = validateBundledResources();

    updateStatus(StatusUtil.getMostSevere(new IStatus[] {
        projectStatus, clientBundleTypeStatus, bundledResourcesStatus}));
  }

  private IStatus validateProject() {
    IProject oldProject = project;
    project = null;

    String projectName = projectField.getText().trim();
    if (projectName.length() == 0) {
      // Empty message will disable the OK button, but not display an error
      return StatusUtilities.newErrorStatus("", GWTPlugin.PLUGIN_ID);
    }

    IProject enteredProject = ResourcesPlugin.getWorkspace().getRoot().getProject(
        projectName);
    if (!enteredProject.exists()) {
      return StatusUtilities.newErrorStatus("Project does not exist",
          GWTPlugin.PLUGIN_ID);
    }

    if (!enteredProject.isOpen()) {
      return StatusUtilities.newErrorStatus("Project is not open",
          GWTPlugin.PLUGIN_ID);
    }

    IJavaProject javaProject = JavaCore.create(enteredProject);
    if (javaProject == null
        || ClientBundleUtilities.findClientBundleType(javaProject) == null) {
      return StatusUtilities.newErrorStatus(
          "Project must be using GWT 2.0 or later to use ClientBundle.",
          GWTPlugin.PLUGIN_ID);
    }

    // Project is valid, so set it
    this.project = enteredProject;

    // Notify others if the project has changed
    if (!JavaUtilities.equalsWithNullCheck(oldProject, enteredProject)) {
      projectChanged();
    }

    return StatusUtilities.OK_STATUS;
  }

}