// Copyright (c) 2010 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.debug.core;

import org.chromium.debug.core.model.VmResourceId;
import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.debug.core.sourcelookup.ISourceContainer;
import org.eclipse.debug.core.sourcelookup.ISourceLookupDirector;
import org.eclipse.debug.core.sourcelookup.containers.ContainerSourceContainer;
import org.eclipse.debug.core.sourcelookup.containers.DefaultSourceContainer;
import org.eclipse.debug.core.sourcelookup.containers.WorkspaceSourceContainer;

/**
 * Eclipse has a standard facility for looking up source file for a debug artifact.
 * LiveEdit feature has an opposite problem: find script in remote VM for a particular js file.
 * This class implements some approach to this problem. An instance of this class corresponds
 * to a particular debug launch.
 */
public class ReverseSourceLookup {
  private final ISourceLookupDirector sourceDirector;

  public ReverseSourceLookup(ISourceLookupDirector sourceDirector) {
    this.sourceDirector = sourceDirector;
  }

  /**
   * Tries to find a corresponding script for a file from a user workspace. The method uses
   * the file name and current source lookup rules to retrieve a resource id, regardless of
   * whether the resource has actually been loaded into the VM (you may want to set a breakpoint
   * on resource before it is actually loaded).
   */
  public VmResourceId findVmResource(IFile sourceFile) throws CoreException {
    for (ISourceContainer container : sourceDirector.getSourceContainers()) {
      VmResourceId scriptName = tryForContainer(sourceFile, container);
      if (scriptName != null) {
        return scriptName;
      }
    }
    return null;
  }

  public static boolean isGoodTargetContainer(ISourceContainer container) {
    return wrapNonVirtualContainer(container) != null;
  }

  private VmResourceId tryForContainer(IFile sourceFile, ISourceContainer container)
      throws CoreException {
    if (container.isComposite() && isSupportedCompositeContainer(container)) {
      ISourceContainer[] subContainers = container.getSourceContainers();
      for (ISourceContainer subContainer : subContainers) {
        VmResourceId res = tryForContainer(sourceFile, subContainer);
        if (res != null) {
          return res;
        }
      }
      return null;
    } else if (container instanceof VProjectSourceContainer) {
      VProjectSourceContainer projectSourceContainer = (VProjectSourceContainer) container;
      return projectSourceContainer.findScriptId(sourceFile);
    } else {
      String name = tryForNonVirtualContainer(sourceFile, container);
      if (name == null) {
        return null;
      }
      return new VmResourceId(name, null);
    }
  }

  /**
   * We use {@link ISourceContainer#getSourceContainers()} method to unwrap internal containers.
   * However it doesn't make sense for all composite containers (some of them may return their
   * subdirectories as containers, which is not what we need).
   */
  private boolean isSupportedCompositeContainer(ISourceContainer container) {
    return container instanceof DefaultSourceContainer;
  }

  /**
   * @param container that may not wrap VProjectSourceContainer
   */
  private String tryForNonVirtualContainer(IFile resource, ISourceContainer container) {
    ContainerWrapper wrapper = wrapNonVirtualContainer(container);
    if (wrapper == null) {
      return null;
    }
    return wrapper.lookup(resource);
  }

  private static ContainerWrapper wrapNonVirtualContainer(ISourceContainer container) {
    if (container instanceof ContainerSourceContainer) {
      final ContainerSourceContainer containerSourceContainer =
          (ContainerSourceContainer) container;
      return new ContainerWrapper() {
        @Override
        public String lookup(IFile resource) {
          return lookupInResourceContainer(resource, containerSourceContainer.getContainer());
        }
      };
    } else if (container instanceof WorkspaceSourceContainer) {
      return new ContainerWrapper() {
        @Override
        public String lookup(IFile resource) {
          return lookupInResourceContainer(resource, ResourcesPlugin.getWorkspace().getRoot());
        }
      };
    } else if (container instanceof SourceNameMapperContainer) {
      SourceNameMapperContainer mappingContainer = (SourceNameMapperContainer) container;
      final ContainerWrapper targetContainerWrapper =
          wrapNonVirtualContainer(mappingContainer.getTargetContainer());
      final String prefix = mappingContainer.getPrefix();
      return new ContainerWrapper() {
        @Override
        public String lookup(IFile resource) {
          String subResult = targetContainerWrapper.lookup(resource);
          if (subResult == null) {
            return null;
          }
          return prefix + subResult;
        }
      };
    }

    return null;
  }

  /**
   * Wraps a container. This interface guarantees that original container with all inner containers
   * are supported by our reversed lookup.
   */
  private interface ContainerWrapper {
    String lookup(IFile resource);
  }

  private static String lookupInResourceContainer(IFile resource, IContainer resourceContainer) {
    IPath resourceFullPath = resource.getFullPath();
    IPath containerFullPath = resourceContainer.getFullPath();
    if (!containerFullPath.isPrefixOf(resourceFullPath)) {
      return null;
    }
    int offset = containerFullPath.segmentCount();
    IPath newPath = resourceFullPath.removeFirstSegments(offset);
    return newPath.toPortableString();
  }
}
