// Copyright (c) 2011 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.model;

import static org.chromium.sdk.util.BasicUtil.join;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.text.MessageFormat;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.chromium.debug.core.ChromiumDebugPlugin;
import org.chromium.debug.core.ScriptNameManipulator;
import org.chromium.debug.core.util.JavaScriptRegExpSupport;
import org.chromium.sdk.Browser;
import org.chromium.sdk.Browser.TabConnector;
import org.chromium.sdk.Browser.TabFetcher;
import org.chromium.sdk.BrowserFactory;
import org.chromium.sdk.BrowserTab;
import org.chromium.sdk.ConnectionLogger;
import org.chromium.sdk.DebugEventListener;
import org.chromium.sdk.JavascriptVm;
import org.chromium.sdk.StandaloneVm;
import org.chromium.sdk.TabDebugEventListener;
import org.chromium.sdk.UnsupportedVersionException;
import org.chromium.sdk.wip.WipBackend;
import org.chromium.sdk.wip.WipBrowser;
import org.chromium.sdk.wip.WipBrowser.WipTabConnector;
import org.chromium.sdk.wip.WipBrowserFactory;
import org.chromium.sdk.wip.WipBrowserTab;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.Status;

public class JavascriptVmEmbedderFactory {
  public static JavascriptVmEmbedder.ConnectionToRemote connectToChromeDevTools(String host,
      int port, NamedConnectionLoggerFactory connectionLoggerFactory,
      final TabSelector tabSelector) throws CoreException {

    SocketAddress address = new InetSocketAddress(host, port);
    final Browser browser = browserCache.getOrCreateBrowser(address, connectionLoggerFactory);

    return connect(browser, tabSelector);
  }

  public static JavascriptVmEmbedder.ConnectionToRemote connectToWipBrowser(String host, int port,
      WipBackend backend,
      final NamedConnectionLoggerFactory browserLoggerFactory,
      final NamedConnectionLoggerFactory tabLoggerFactory,
      WipTabSelector tabSelector) throws CoreException {

    InetSocketAddress address = new InetSocketAddress(host, port);
    WipBrowserFactory.LoggerFactory factory = new WipBrowserFactory.LoggerFactory() {
      @Override
      public ConnectionLogger newBrowserConnectionLogger() {
        return browserLoggerFactory.createLogger("Connection to browser");
      }

      @Override
      public ConnectionLogger newTabConnectionLogger() {
        return browserLoggerFactory.createLogger("Connection to tab");
      }
    };

    final WipBrowser browser =
        WipBrowserFactory.INSTANCE.createBrowser(address, factory);

    return connectWip(browser, backend, tabSelector);
  }

  private static JavascriptVmEmbedder.ConnectionToRemote connect(Browser browser,
      final TabSelector tabSelector) throws CoreException {

    final TabFetcher tabFetcher;
    try {
      tabFetcher = browser.createTabFetcher();
    } catch (UnsupportedVersionException e) {
      throw newCoreException(e);
    } catch (IOException e) {
      throw newCoreException(e);
    }

    return new JavascriptVmEmbedder.ConnectionToRemote() {
      public JavascriptVmEmbedder.VmConnector selectVm() throws CoreException {
        Browser.TabConnector targetTabConnector;
        try {
          targetTabConnector = tabSelector.selectTab(tabFetcher);
        } catch (IOException e) {
          throw newCoreException("Failed to get tabs for debugging", e);
        }
        if (targetTabConnector == null) {
          return null;
        }

        return new EmbeddingTabConnectorImpl(targetTabConnector);
      }

      public void disposeConnection() {
        tabFetcher.dismiss();
      }
    };
  }

  private static JavascriptVmEmbedder.ConnectionToRemote connectWip(final WipBrowser browser,
      final WipBackend backend, final WipTabSelector tabSelector) throws CoreException {
    return new JavascriptVmEmbedder.ConnectionToRemote() {
      public JavascriptVmEmbedder.VmConnector selectVm() throws CoreException {
        WipTabSelector.BrowserAndBackend browserAndBackend =
            new WipTabSelector.BrowserAndBackend() {
          @Override public WipBrowser getBrowser() {
            return browser;
          }
          @Override public WipBackend getBackend() {
            return backend;
          }
        };
        WipBrowser.WipTabConnector targetTabConnector;
        try {
          targetTabConnector = tabSelector.selectTab(browserAndBackend);
        } catch (IOException e) {
          throw newCoreException("Failed to get tabs for debugging", e);
        }
        if (targetTabConnector == null) {
          return null;
        }

        return new WipEmbeddingTabConnector(targetTabConnector, backend.getId());
      }

      public void disposeConnection() {
      }
    };
  }

  private static abstract class EmbeddingTabConnectorBase<TC>
      implements JavascriptVmEmbedder.VmConnector {
    private final TC targetTabConnector;

    EmbeddingTabConnectorBase(TC targetTabConnector) {
      this.targetTabConnector = targetTabConnector;
    }

    protected TC getTabConnector() {
      return targetTabConnector;
    }

    public JavascriptVmEmbedder attach(final JavascriptVmEmbedder.Listener embedderListener,
        final DebugEventListener debugEventListener) throws CoreException {
      TabDebugEventListener tabDebugEventListener = new TabDebugEventListener() {
        public DebugEventListener getDebugEventListener() {
          return debugEventListener;
        }
        public void closed() {
          embedderListener.closed();
        }
        public void navigated(String newUrl) {
          embedderListener.reset();
        }
      };

      return attach(tabDebugEventListener);
    }

    protected abstract JavascriptVmEmbedder attach(TabDebugEventListener tabDebugEventListener)
        throws CoreException;

    protected static abstract class EmbedderBase implements JavascriptVmEmbedder {
      @Override
      public ScriptNameManipulator getScriptNameManipulator() {
        return BROWSER_SCRIPT_NAME_MANIPULATOR;
      }
    }

    private static final ScriptNameManipulator BROWSER_SCRIPT_NAME_MANIPULATOR =
        new ScriptNameManipulator() {
      @Override
      public FilePath getFileName(String scriptName) {
        String filePath;
        try {
          URI uri = new URI(scriptName);
          filePath = uri.getPath();
        } catch (URISyntaxException e) {
          filePath = scriptName;
        }
        return new StringBasedFileName(filePath);
      }

      @Override
      public ScriptNamePattern createPattern(List<String> components) {
        String pathString = join(components, "/");
        return new ScriptNamePattern(
            JavaScriptRegExpSupport.encodeLiteral(pathString) + "/?($|\\?)");
      }
    };
  }

  private static class EmbeddingTabConnectorImpl
      extends EmbeddingTabConnectorBase<Browser.TabConnector> {
    EmbeddingTabConnectorImpl(TabConnector targetTabConnector) {
      super(targetTabConnector);
    }

    @Override
    protected JavascriptVmEmbedder attach(TabDebugEventListener tabDebugEventListener)
        throws CoreException {
      final BrowserTab browserTab;
      try {
        browserTab = getTabConnector().attach(tabDebugEventListener);
      } catch (IOException e) {
        throw newCoreException("Failed to connect to browser tab", e);
      }
      return new EmbedderBase() {
        public JavascriptVm getJavascriptVm() {
          return browserTab;
        }

        public String getTargetName() {
          return Messages.DebugTargetImpl_TargetName;
        }


        public String getThreadName() {
          return browserTab.getUrl();
        }
      };
    }
  }

  private static class WipEmbeddingTabConnector
      extends EmbeddingTabConnectorBase<WipBrowser.WipTabConnector> {
    private final String backendId;

    WipEmbeddingTabConnector(WipTabConnector targetTabConnector, String backendId) {
      super(targetTabConnector);
      this.backendId = backendId;
    }

    @Override
    protected JavascriptVmEmbedder attach(TabDebugEventListener tabDebugEventListener)
        throws CoreException {
      final WipBrowserTab browserTab;
      try {
        browserTab = getTabConnector().attach(tabDebugEventListener);
      } catch (IOException e) {
        throw newCoreException("Failed to connect to browser tab: " + e.getMessage(), e);
      }
      return new EmbedderBase() {
        public JavascriptVm getJavascriptVm() {
          return browserTab.getJavascriptVm();
        }

        public String getTargetName() {
          return Messages.DebugTargetImpl_TargetName + " # " + backendId;
        }

        public String getThreadName() {
          return browserTab.getUrl();
        }
      };
    }
  }

  public static JavascriptVmEmbedder.ConnectionToRemote connectToStandalone(String host, int port,
      NamedConnectionLoggerFactory connectionLoggerFactory) {
    SocketAddress address = new InetSocketAddress(host, port);
    ConnectionLogger connectionLogger =
      connectionLoggerFactory.createLogger(address.toString());
    final StandaloneVm standaloneVm = BrowserFactory.getInstance().createStandalone(address,
        connectionLogger);

    return new JavascriptVmEmbedder.ConnectionToRemote() {
      public JavascriptVmEmbedder.VmConnector selectVm() {
        return new JavascriptVmEmbedder.VmConnector() {
          public JavascriptVmEmbedder attach(JavascriptVmEmbedder.Listener embedderListener,
              DebugEventListener debugEventListener)
              throws CoreException {
            embedderListener = null;
          //+ @since 0.9 modified exception message string
            String ERROR_STRING = "Nodeclipse/chromedevtools failed to connect to Standalone V8 VM "
            		+"( Check Help (F1) and Support http://www.nodeclipse.org/#support )\n. Info:"; 
            try {
              standaloneVm.attach(debugEventListener);
            } catch (IOException e) {
              throw newCoreException(ERROR_STRING, e);
            } catch (UnsupportedVersionException e) {
              throw newCoreException(ERROR_STRING, e);
            }
            return new JavascriptVmEmbedder() {
              public JavascriptVm getJavascriptVm() {
                return standaloneVm;
              }
              public String getTargetName() {
                String embedderName = standaloneVm.getEmbedderName();
                String vmVersion = standaloneVm.getVmVersion();
                String disconnectReason = standaloneVm.getDisconnectReason();
                String targetTitle;
                if (embedderName == null) {
                  targetTitle = ""; //$NON-NLS-1$
                } else {
                  targetTitle = MessageFormat.format(
                      Messages.JavascriptVmEmbedderFactory_TargetName0, embedderName, vmVersion);
                }
                boolean isAttached = standaloneVm.isAttached();
                if (!isAttached) {
                  String disconnectMessage;
                  if (disconnectReason == null) {
                    disconnectMessage = Messages.JavascriptVmEmbedderFactory_Terminated;
                  } else {
                    disconnectMessage = MessageFormat.format(
                        Messages.JavascriptVmEmbedderFactory_TerminatedWithReason,
                        disconnectReason);
                  }
                  targetTitle = "<" + disconnectMessage + "> " + targetTitle;
                }
                return targetTitle;
              }
              public String getThreadName() {
                return ""; //$NON-NLS-1$
              }

              @Override
              public ScriptNameManipulator getScriptNameManipulator() {
                return STANDALONE_SCRIPT_NAME_MANIPULATOR;
              }
            };
          }
        };
      }

      public void disposeConnection() {
        // Nothing to do. We do not take connection for ConnectionToRemote.
      }
    };
  }

  private static final ScriptNameManipulator STANDALONE_SCRIPT_NAME_MANIPULATOR =
      new ScriptNameManipulator() {
    @Override
    public FilePath getFileName(String scriptName) {
      return new StringBasedFileName(scriptName);
    }

    @Override
    public ScriptNamePattern createPattern(List<String> components) {
      String pathString = join(components, "/");
      return new ScriptNamePattern(JavaScriptRegExpSupport.encodeLiteral(pathString) + "/?$");
    }
  };

  private static CoreException newCoreException(String message, Throwable cause) {
    return new CoreException(
        new Status(Status.ERROR, ChromiumDebugPlugin.PLUGIN_ID, message, cause));
  }
  private static CoreException newCoreException(Exception e) {
    return new CoreException(
        new Status(Status.ERROR, ChromiumDebugPlugin.PLUGIN_ID,
            "Failed to connect to the remote browser", e));
  }

  private static final BrowserCache browserCache = new BrowserCache();
  /**
   * Cache of browser instances.
   */
  private static class BrowserCache {

    /**
     * Tries to return already created instance of Browser connected to {@code address}
     * or create new instance.
     * However, it creates a new instance each time that {@code ConnectionLogger} is not null
     * (because you cannot add connection logger to existing connection).
     * @throws CoreException if browser can't be created because of conflict with connectionLogger
     */
    synchronized Browser getOrCreateBrowser(final SocketAddress address,
        final NamedConnectionLoggerFactory connectionLoggerFactory) throws CoreException {
      Browser result = address2Browser.get(address);
      if (result == null) {

        ConnectionLogger.Factory wrappedFactory = new ConnectionLogger.Factory() {
          public ConnectionLogger newConnectionLogger() {
            return connectionLoggerFactory.createLogger(address.toString());
          }
        };
        result = createBrowserImpl(address, wrappedFactory);

        address2Browser.put(address, result);
      }
      return result;
    }
    private Browser createBrowserImpl(SocketAddress address,
        ConnectionLogger.Factory connectionLoggerFactory) {
      return BrowserFactory.getInstance().create(address, connectionLoggerFactory);
    }

    private final Map<SocketAddress, Browser> address2Browser =
        new HashMap<SocketAddress, Browser>();
  }

  private static class StringBasedFileName implements ScriptNameManipulator.FilePath {
    private final String fullName;
    private final int lastPos;

    public StringBasedFileName(String fullName) {
      this.fullName = fullName;
      this.lastPos = fullName.lastIndexOf('/');
    }

    @Override
    public Iterator<String> iterator() {
      return new Iterator<String>() {
        private int currentPos = lastPos;
        @Override
        public void remove() {
          throw new UnsupportedOperationException();
        }

        @Override
        public String next() {
          int nextPos = fullName.lastIndexOf('/', currentPos - 1);
          String result;
          if (nextPos == -1) {
            result = fullName.substring(0, currentPos);
          } else {
            result = fullName.substring(nextPos + 1, currentPos);
          }
          currentPos = nextPos;
          return result;
        }

        @Override
        public boolean hasNext() {
          return currentPos != -1;
        }
      };
    }

    @Override
    public String getLastComponent() {
      if (lastPos == -1) {
        return fullName;
      } else {
        return fullName.substring(lastPos + 1);
      }
    }
  }
}
