//============================================================================
//
// Copyright (C) 2002-2006  David Schneider, Lars Ködderitzsch, Fabrice Bellingard
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
//
//============================================================================

package net.sf.eclipsecs.ui.stats.views.internal;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;

import net.sf.eclipsecs.core.builder.CheckstyleMarker;
import net.sf.eclipsecs.ui.CheckstyleUIPlugin;

import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.jface.dialogs.IDialogSettings;
import org.eclipse.ui.IWorkingSet;

/**
 * Filter class for Checkstyle markers. This filter is used by the Checkstyle statistics views.
 *
 * @author Lars Ködderitzsch
 */
public class CheckstyleMarkerFilter implements Cloneable {

  //
  // constants
  //

  private static final String TAG_DIALOG_SECTION = "filter"; //$NON-NLS-1$

  private static final String TAG_ENABLED = "enabled"; //$NON-NLS-1$

  private static final String TAG_ON_RESOURCE = "onResource"; //$NON-NLS-1$

  private static final String TAG_WORKING_SET = "workingSet"; //$NON-NLS-1$

  private static final String TAG_SELECT_BY_SEVERITY = "selectBySeverity"; //$NON-NLS-1$

  private static final String TAG_SEVERITY = "severity"; //$NON-NLS-1$

  private static final String TAG_SELECT_BY_REGEX = "selectByRegex"; //$NON-NLS-1$

  private static final String TAG_REGULAR_EXPRESSIONS = "regularExpressions"; //$NON-NLS-1$

  public static final int ON_ANY_RESOURCE = 0;

  public static final int ON_SELECTED_RESOURCE_ONLY = 1;

  public static final int ON_SELECTED_RESOURCE_AND_CHILDREN = 2;

  public static final int ON_ANY_RESOURCE_OF_SAME_PROJECT = 3;

  public static final int ON_WORKING_SET = 4;

  private static final int DEFAULT_SEVERITY = 0;

  public static final int SEVERITY_ERROR = 1 << 2;

  public static final int SEVERITY_WARNING = 1 << 1;

  public static final int SEVERITY_INFO = 1 << 0;

  private static final int DEFAULT_ON_RESOURCE = ON_ANY_RESOURCE;

  private static final boolean DEFAULT_SELECT_BY_SEVERITY = false;

  private static final boolean DEFAULT_ACTIVATION_STATUS = true;

  //
  // attributes
  //

  /** Determines if this filter is enabled. */
  private boolean mEnabled;

  /** The selection mode. */
  private int mOnResource;

  /** The selected working set. */
  private IWorkingSet mWorkingSet;

  /** Flags if the severity filtering is active. */
  private boolean mSelectBySeverity;

  /** The selected severity. */
  private int mSeverity;

  /** The focused resources within the current workbench page. */
  private IResource[] mFocusResources;

  /** Flags if the regex filter is enabled. */
  private boolean mFilterByRegex;

  /** List of regular expressions used to filter messages. */
  private List<String> mFilterRegex;

  //
  // methods
  //

  /**
   * Searches the workspace for markers that pass this filter.
   *
   * @param mon
   *          the progress monitor
   * @return the array of Checkstyle markers that pass this filter.
   * @throws CoreException
   *           an unexpected error occurred
   */
  public IMarker[] findMarkers(IProgressMonitor mon) throws CoreException {

    List<IMarker> unfiltered = Collections.emptyList();

    if (!isEnabled()) {
      unfiltered = findCheckstyleMarkers(
              new IResource[] { ResourcesPlugin.getWorkspace().getRoot() },
              IResource.DEPTH_INFINITE, mon);
    } else {

      switch (getOnResource()) {
        case ON_ANY_RESOURCE: {
          unfiltered = findCheckstyleMarkers(
                  new IResource[] { ResourcesPlugin.getWorkspace().getRoot() },
                  IResource.DEPTH_INFINITE, mon);
          break;
        }
        case ON_SELECTED_RESOURCE_ONLY: {
          unfiltered = findCheckstyleMarkers(mFocusResources, IResource.DEPTH_ZERO, mon);
          break;
        }
        case ON_SELECTED_RESOURCE_AND_CHILDREN: {
          unfiltered = findCheckstyleMarkers(mFocusResources, IResource.DEPTH_INFINITE, mon);
          break;
        }
        case ON_ANY_RESOURCE_OF_SAME_PROJECT: {
          unfiltered = findCheckstyleMarkers(getProjects(mFocusResources), IResource.DEPTH_INFINITE,
                  mon);
          break;
        }
        case ON_WORKING_SET: {
          unfiltered = findCheckstyleMarkers(getResourcesInWorkingSet(mWorkingSet),
                  IResource.DEPTH_INFINITE, mon);
          break;
        }
        default: {
          break;
        }
      }
    }

    if (unfiltered == null) {
      unfiltered = Collections.emptyList();
    }

    return unfiltered.toArray(new IMarker[unfiltered.size()]);
  }

  /**
   * @return
   *         <ul>
   *         <li><code>MarkerFilter.ON_ANY_RESOURCE</code> if showing items associated with any
   *         resource.</li>
   *         <li><code>MarkerFilter.ON_SELECTED_RESOURCE_ONLY</code> if showing items associated
   *         with the selected resource within the workbench.</li>
   *         <li><code>MarkerFilter.ON_SELECTED_RESOURCE_AND_CHILDREN</code> if showing items
   *         associated with the selected resource within the workbench and its children.</li>
   *         <li><code>MarkerFilter.ON_ANY_RESOURCE_OF_SAME_PROJECT</code> if showing items in the
   *         same project as the selected resource within the workbench.</li>
   *         <li><code>MarkerFilter.ON_WORKING_SET</code> if showing items in some working set.</li>
   *         </ul>
   */
  public int getOnResource() {
    return mOnResource;
  }

  /**
   * Sets the type of filtering by selection.
   *
   * @param onResource
   *          must be one of:
   *          <ul>
   *          <li><code>MarkerFilter.ON_ANY_RESOURCE</code></li>
   *          <li><code>MarkerFilter.ON_SELECTED_RESOURCE_ONLY</code></li>
   *          <li><code>MarkerFilter.ON_SELECTED_RESOURCE_AND_CHILDREN</code></li>
   *          <li><code>MarkerFilter.ON_ANY_RESOURCE_OF_SAME_PROJECT</code></li>
   *          <li><code>MarkerFilter.ON_WORKING_SET</code></li>
   *          </ul>
   */
  public void setOnResource(int onResource) {
    if (onResource >= ON_ANY_RESOURCE && onResource <= ON_WORKING_SET) {
      this.mOnResource = onResource;
    }
  }

  /**
   * Returns the selected resource.
   *
   * @return the selected resource(s) within the workbench.
   */
  public IResource[] getFocusResource() {
    return mFocusResources;
  }

  /**
   * Sets the focused resources.
   *
   * @param resources
   *          the focused resources
   */
  public void setFocusResource(IResource[] resources) {
    mFocusResources = resources;
  }

  /**
   * @return
   *         <ul>
   *         <li><code>true</code> if the filter is enabled.</li>
   *         <li><code>false</code> if the filter is not enabled.</li>
   *         </ul>
   */
  public boolean isEnabled() {
    return mEnabled;
  }

  /**
   * Sets the enablement state of the filter.
   *
   * @param enabled
   *          the enablement
   */
  public void setEnabled(boolean enabled) {
    this.mEnabled = enabled;
  }

  /**
   * Returns the current working set.
   *
   * @return the current working set or <code>null</code> if no working set is defined.
   */
  public IWorkingSet getWorkingSet() {
    return mWorkingSet;
  }

  /**
   * Sets the current working set.
   *
   * @param workingSet
   *          the working set
   */
  public void setWorkingSet(IWorkingSet workingSet) {
    this.mWorkingSet = workingSet;
  }

  /**
   * Returns if the markers will be selected by severity.
   *
   * @return <code>true</code> if markers will be selected by severity
   */
  public boolean getSelectBySeverity() {
    return mSelectBySeverity;
  }

  /**
   * Sets if the markers will be selected by severity.
   *
   * @param selectBySeverity
   *          <code>true</code> if markers will be selected by severity
   */
  public void setSelectBySeverity(boolean selectBySeverity) {
    this.mSelectBySeverity = selectBySeverity;
  }

  /**
   * Returns the severity.
   *
   * @return the severity
   */
  public int getSeverity() {
    return mSeverity;
  }

  /**
   * Sets the severity.
   *
   * @param severity
   *          the severity
   */
  public void setSeverity(int severity) {
    this.mSeverity = severity;
  }

  /**
   * Returns if the regex filter is enabled.
   *
   * @return <code>true</code> if the regex filter is enabled
   */
  public boolean isFilterByRegex() {
    return mFilterByRegex;
  }

  /**
   * Sets if the regex filter is enabled.
   *
   * @param filterByRegex
   *          <code>true</code> if messages are filtered by the regular expressions
   */
  public void setFilterByRegex(boolean filterByRegex) {
    mFilterByRegex = filterByRegex;
  }

  /**
   * Returns the regular expressions.
   *
   * @return the regular expressions
   */
  public List<String> getFilterRegex() {
    return mFilterRegex;
  }

  /**
   * Sets the list of regular expressions.
   *
   * @param filterRegex
   *          the list of regular expression to filter by
   */
  public void setFilterRegex(List<String> filterRegex) {
    mFilterRegex = filterRegex;
  }

  /**
   * Restores the state of the filter from the given dialog settings.
   *
   * @param dialogSettings
   *          the dialog settings
   */
  public void restoreState(IDialogSettings dialogSettings) {
    resetState();
    IDialogSettings settings = dialogSettings.getSection(TAG_DIALOG_SECTION);

    if (settings != null) {

      String setting = settings.get(TAG_ENABLED);
      if (setting != null) {
        mEnabled = Boolean.valueOf(setting).booleanValue();
      }

      setting = settings.get(TAG_ON_RESOURCE);
      if (setting != null) {
        try {
          mOnResource = Integer.parseInt(setting);
        } catch (NumberFormatException e) {
          // ignore and use default value
        }
      }

      setting = settings.get(TAG_WORKING_SET);
      if (setting != null) {
        setWorkingSet(CheckstyleUIPlugin.getDefault().getWorkbench().getWorkingSetManager()
                .getWorkingSet(setting));
      }

      setting = settings.get(TAG_SELECT_BY_SEVERITY);
      if (setting != null) {
        mSelectBySeverity = Boolean.valueOf(setting).booleanValue();
      }

      setting = settings.get(TAG_SEVERITY);
      if (setting != null) {
        try {
          mSeverity = Integer.parseInt(setting);
        } catch (NumberFormatException e) {
          // ignore and use default value
        }
      }

      setting = settings.get(TAG_SELECT_BY_REGEX);
      if (setting != null) {
        mFilterByRegex = Boolean.valueOf(setting).booleanValue();
      }

      String[] regex = settings.getArray(TAG_REGULAR_EXPRESSIONS);
      if (regex != null) {
        mFilterRegex = Arrays.asList(regex);
      }
    }
  }

  /**
   * Saves the state of the filter into the given dialog settings.
   *
   * @param dialogSettings
   *          the dialog settings
   */
  public void saveState(IDialogSettings dialogSettings) {
    if (dialogSettings != null) {
      IDialogSettings settings = dialogSettings.getSection(TAG_DIALOG_SECTION);

      if (settings == null) {
        settings = dialogSettings.addNewSection(TAG_DIALOG_SECTION);
      }

      settings.put(TAG_ENABLED, mEnabled);
      settings.put(TAG_ON_RESOURCE, mOnResource);

      if (mWorkingSet != null) {
        settings.put(TAG_WORKING_SET, mWorkingSet.getName());
      }

      settings.put(TAG_SELECT_BY_SEVERITY, mSelectBySeverity);
      settings.put(TAG_SEVERITY, mSeverity);

      settings.put(TAG_SELECT_BY_REGEX, mFilterByRegex);

      if (mFilterRegex != null) {
        settings.put(TAG_REGULAR_EXPRESSIONS,
                mFilterRegex.toArray(new String[mFilterRegex.size()]));
      }
    }
  }

  /**
   * Restores the default state of the filter.
   */
  public void resetState() {
    mEnabled = DEFAULT_ACTIVATION_STATUS;
    mOnResource = DEFAULT_ON_RESOURCE;
    setWorkingSet(null);
    mSelectBySeverity = DEFAULT_SELECT_BY_SEVERITY;
    mSeverity = DEFAULT_SEVERITY;
    mFilterByRegex = false;
    mFilterRegex = new ArrayList<>();
  }

  /**
   * Returns a list of all markers in the given set of resources.
   *
   * @param resources
   *          the resources
   * @param depth
   *          the depth with which the markers are searched
   * @param mon
   *          the progress monitor
   * @throws CoreException
   *           if the resource does not exist or the project is not open
   */
  private List<IMarker> findCheckstyleMarkers(IResource[] resources, int depth, IProgressMonitor mon)
          throws CoreException {
    if (resources == null) {
      return Collections.emptyList();
    }

    List<IMarker> resultList = new ArrayList<>(resources.length * 2);

    for (int i = 0, size = resources.length; i < size; i++) {
      if (resources[i].isAccessible()) {
        Collection<IMarker> markers = Arrays
                .asList(resources[i].findMarkers(CheckstyleMarker.MARKER_ID, true, depth));

        resultList.addAll(markers);
      }
    }
    
    if (!mEnabled) {
      return resultList;
    }

    if (mSelectBySeverity) {
      // further filter the markers by severity
      int size = resultList.size();
      for (int i = size - 1; i >= 0; i--) {
        IMarker marker = resultList.get(i);
        if (!selectBySeverity(marker)) {
          resultList.remove(i);
        }
      }
    }

    if (mFilterByRegex) {
      // further filter the markers by regular expressions
      int size = resultList.size();
      for (int i = size - 1; i >= 0; i--) {
        IMarker marker = resultList.get(i);
        if (!selectByRegex(marker)) {
          resultList.remove(i);
        }
      }
    }

    return resultList;
  }

  /**
   * Selects markers by its severity.
   *
   * @param item
   *          the marker
   * @return <code>true</code> if the marker is selected
   */
  private boolean selectBySeverity(IMarker item) {
    if (mSelectBySeverity) {
      int markerSeverity = item.getAttribute(IMarker.SEVERITY, -1);
      if (markerSeverity == IMarker.SEVERITY_ERROR) {
        return (mSeverity & SEVERITY_ERROR) > 0;
      } else if (markerSeverity == IMarker.SEVERITY_WARNING) {
        return (mSeverity & SEVERITY_WARNING) > 0;
      } else if (markerSeverity == IMarker.SEVERITY_INFO) {
        return (mSeverity & SEVERITY_INFO) > 0;
      }
    }

    return true;
  }

  /**
   * Selects marker by matching the message against regular expressions.
   *
   * @param item
   *          the marker
   * @return <code>true</code> if the marker is selected
   */
  private boolean selectByRegex(IMarker item) {

    if (mFilterByRegex) {

      int size = mFilterRegex != null ? mFilterRegex.size() : 0;
      for (int i = 0; i < size; i++) {

        String regex = mFilterRegex.get(i);

        String message = item.getAttribute(IMarker.MESSAGE, null);

        if (message != null && message.matches(regex)) {
          return false;
        }
      }
    }
    return true;

  }

  /**
   * Returns the set of projects that contain the given set of resources.
   *
   * @param resources
   *          the resources
   * @return the array of projects for the given resources
   */
  private static IProject[] getProjects(IResource[] resources) {
    Collection<IProject> projects = getProjectsAsCollection(resources);
    return projects.toArray(new IProject[projects.size()]);
  }

  /**
   * Returns the set of projects that contain the given set of resources.
   *
   * @param resources
   *          the resources
   * @return the collection of projects for the given resources
   */
  public static Collection<IProject> getProjectsAsCollection(IResource[] resources) {
    HashSet<IProject> projects = new HashSet<>();

    for (int idx = 0, size = resources != null ? resources.length : 0; idx < size; idx++) {
      projects.add(resources[idx].getProject());
    }
    return projects;
  }

  /**
   * Returns all resources within the working set.
   *
   * @param workingSet
   *          the working set
   * @return the array of resources from the given working set
   */
  private static IResource[] getResourcesInWorkingSet(IWorkingSet workingSet) {
    if (workingSet == null) {
      return new IResource[0];
    }

    IAdaptable[] elements = workingSet.getElements();
    List<IResource> result = new ArrayList<>(elements.length);

    for (int idx = 0; idx < elements.length; idx++) {
      @SuppressWarnings("cast")
      IResource next = (IResource) elements[idx].getAdapter(IResource.class);

      if (next != null) {
        result.add(next);
      }
    }

    return result.toArray(new IResource[result.size()]);
  }

  /**
   * {@inheritDoc}
   *
   * @see java.lang.Object#clone()
   */
  @Override
  public Object clone() {
    try {
      return super.clone();
    } catch (CloneNotSupportedException e) {
      throw new InternalError(); // this should never happen
    }
  }
}