/*
 * Copyright 2012-2014 eBay Software Foundation and selendroid committers.
 * 
 * Licensed 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 io.selendroid.server.model;

import android.app.Activity;
import android.content.res.Resources.Theme;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Point;
import android.graphics.drawable.Drawable;
import android.provider.Settings;
import android.view.Display;
import android.view.View;
import android.webkit.WebView;
import io.selendroid.server.ServerInstrumentation;
import io.selendroid.server.android.*;
import io.selendroid.server.android.internal.Dimension;
import io.selendroid.server.common.exceptions.NoSuchElementException;
import io.selendroid.server.common.exceptions.SelendroidException;
import io.selendroid.server.common.utils.CallLogEntry;
import io.selendroid.server.inspector.TreeUtil;
import io.selendroid.server.model.internal.AbstractNativeElementContext;
import io.selendroid.server.model.internal.AbstractWebElementContext;
import io.selendroid.server.model.internal.WebViewHandleMapper;
import io.selendroid.server.model.internal.execute_native.*;
import io.selendroid.server.model.js.AndroidAtoms;
import io.selendroid.server.util.Preconditions;
import io.selendroid.server.util.SelendroidLogger;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.IOException;
import java.util.*;


public class DefaultSelendroidDriver implements SelendroidDriver {
  public static final String BROWSER_NAME = "browserName";
  public static final String PLATFORM = "platform";
  public static final String SUPPORTS_JAVASCRIPT = "javascriptEnabled";
  public static final String TAKES_SCREENSHOT = "takesScreenshot";
  public static final String VERSION = "version";
  public static final String SUPPORTS_ALERTS = "handlesAlerts";
  public static final String ROTATABLE = "rotatable";
  public static final String ACCEPT_SSL_CERTS = "acceptSslCerts";
  public static final String SUPPORTS_NETWORK_CONNECTION = "networkConnectionEnabled";
  private boolean done = false;
  private SearchContext nativeSearchScope = null;
  private SearchContext webviewSearchScope = null;
  private ServerInstrumentation serverInstrumentation = null;
  private Session session = null;
  private final Object syncObject = new Object();
  private KeySender keySender = null;
  private SelendroidNativeDriver selendroidNativeDriver = null;
  private SelendroidWebDriver selendroidWebDriver = null;
  private TrackBall trackBall = null;
  private String activeWindowType = null;
  private long scriptTimeout = 0L;

  private Map<String, NativeExecuteScript> nativeExecuteScriptMap =
      new HashMap<String, NativeExecuteScript>();


  public DefaultSelendroidDriver(ServerInstrumentation instrumentation) {
    serverInstrumentation = instrumentation;
    keySender = new InstrumentedKeySender(serverInstrumentation.getInstrumentation());
  }

  /*
   * (non-Javadoc)
   * 
   * @see
   * org.openqa.selenium.android.server.AndroidDriver#findElement(org.openqa.selenium.android.common
   * .model.By)
   */
  @Override
  public AndroidElement findElement(final By by) {
    if (by == null) {
      throw new IllegalArgumentException("By cannot be null.");
    }

    SearchContext context = getSearchContext();
    AndroidElement found = by.findElement(context);
    long timeout = getTimeout();

    while (found == null && (System.currentTimeMillis() < timeout)) {
      sleepQuietly(AndroidWait.DEFAULT_SLEEP_INTERVAL);
      found = by.findElement(context);
    }
    return found;
  }

  private long getTimeout() {
    return System.currentTimeMillis() + serverInstrumentation.getAndroidWait().getTimeoutInMillis();
  }

  /*
   * (non-Javadoc)
   * 
   * @see
   * org.openqa.selenium.android.server.AndroidDriver#findElements(org.openqa.selenium.android.common
   * .model.By)
   */
  @Override
  public List<AndroidElement> findElements(By by) {
    if (by == null) {
      throw new IllegalArgumentException("By cannot be null.");
    }
    long timeout = getTimeout();
    SearchContext context = getSearchContext();

    List<AndroidElement> found = by.findElements(context);
    while (found.isEmpty() && (System.currentTimeMillis() < timeout)) {
      sleepQuietly(AndroidWait.DEFAULT_SLEEP_INTERVAL);
      found = by.findElements(context);
    }
    return found;
  }

  private SearchContext getSearchContext() {
    if (isNativeWindowMode()) {
      Preconditions.checkNotNull(nativeSearchScope);
      return nativeSearchScope;
    } else {
      Preconditions.checkNotNull(webviewSearchScope);
      return webviewSearchScope;
    }

  }

  /*
   * (non-Javadoc)
   * 
   * @see org.openqa.selenium.android.server.AndroidDriver#getSession()
   */
  @Override
  public Session getSession() {
    return session;
  }

  /*
   * (non-Javadoc)
   * 
   * @see org.openqa.selenium.android.server.AndroidDriver#getSessionCapabilities(java.lang.String)
   */
  @Override
  public JSONObject getSessionCapabilities(String sessionId) {
    SelendroidLogger.info("session: " + sessionId);

    JSONObject copy;
    try {
      JSONObject capabilities = session.getCapabilities();
      if (capabilities != null) {
        copy = new JSONObject(capabilities.toString());
      } else {
        copy = new JSONObject();
      }
      copy.put(TAKES_SCREENSHOT, true);
      copy.put(BROWSER_NAME, "selendroid");
      copy.put("automationName", "selendroid");
      copy.put("platformName", "android");
      copy.put("platformVersion", serverInstrumentation.getOsVersion());
      copy.put(ROTATABLE, true);
      copy.put(PLATFORM, "android");
      copy.put(SUPPORTS_ALERTS, true);
      copy.put(SUPPORTS_JAVASCRIPT, true);
      copy.put(SUPPORTS_NETWORK_CONNECTION, true);
      copy.put("version", serverInstrumentation.getServerVersion());
      copy.put(ACCEPT_SSL_CERTS, true);
      SelendroidLogger.info("capabilities: " + copy);
      return copy;
    } catch (JSONException e) {
      throw new SelendroidException(e);
    }
  }

  public static void sleepQuietly(long ms) {
    try {
      Thread.sleep(ms);
    } catch (InterruptedException cause) {
      Thread.currentThread().interrupt();
      throw new SelendroidException(cause);
    }
  }

  /*
   * (non-Javadoc)
   * 
   * @see org.openqa.selenium.android.server.AndroidDriver#stopSession()
   */
  @Override
  public void stopSession() {
    serverInstrumentation.finishAllActivities();
    this.activeWindowType = WindowType.NATIVE_APP.name();
    this.session = null;
    nativeSearchScope = null;
    selendroidNativeDriver = null;
    selendroidWebDriver = null;
    webviewSearchScope = null;
  }

  /*
   * 
   * 
   * @see org.openqa.selenium.android.server.AndroidDriver#takeScreenshot()
   */
  @Override
  @SuppressWarnings("deprecation")
  public byte[] takeScreenshot() {
    ViewHierarchyAnalyzer viewAnalyzer = ViewHierarchyAnalyzer.getDefaultInstance();

    // TODO ddary review later, but with getRecentDecorView() it seems to work better
    // long drawingTime = 0;
    // View container = null;
    // for (View view : viewAnalyzer.getTopLevelViews()) {
    // if (view != null && view.isShown() && view.hasWindowFocus()
    // && view.getDrawingTime() > drawingTime) {
    // container = view;
    // drawingTime = view.getDrawingTime();
    // }
    // }
    // final View mainView = container;
    final View mainView = viewAnalyzer.getRecentDecorView();
    if (mainView == null) {
      throw new SelendroidException("No open windows.");
    }
    done = false;
    long end =
        System.currentTimeMillis() + serverInstrumentation.getAndroidWait().getTimeoutInMillis();
    final byte[][] rawPng = new byte[1][1];
    serverInstrumentation.getCurrentActivity().runOnUiThread(new Runnable() {
      public void run() {
        synchronized (syncObject) {
          Display display =
              serverInstrumentation.getCurrentActivity().getWindowManager().getDefaultDisplay();
          Point size = new Point();
          try {
            display.getSize(size);
          } catch (NoSuchMethodError ignore) { // Older than api level 13
            size.x = display.getWidth();
            size.y = display.getHeight();
          }

          // Get root view
          View view = mainView.getRootView();

          // Create the bitmap to use to draw the screenshot
          final Bitmap bitmap = Bitmap.createBitmap(size.x, size.y, Bitmap.Config.ARGB_8888);
          final Canvas canvas = new Canvas(bitmap);

          // Get current theme to know which background to use
          final Activity activity = serverInstrumentation.getCurrentActivity();
          final Theme theme = activity.getTheme();
          final TypedArray ta =
              theme.obtainStyledAttributes(new int[] {android.R.attr.windowBackground});
          final int res = ta.getResourceId(0, 0);
          final Drawable background = activity.getResources().getDrawable(res);

          // Draw background
          background.draw(canvas);

          // Draw views
          view.draw(canvas);

          ByteArrayOutputStream stream = new ByteArrayOutputStream();
          if (!bitmap.compress(Bitmap.CompressFormat.PNG, 70, stream)) {
            throw new RuntimeException("Error while compressing screenshot image.");
          }
          try {
            stream.flush();
            stream.close();
          } catch (IOException e) {
            throw new RuntimeException("I/O Error while capturing screenshot: " + e.getMessage());
          } finally {
            Closeable closeable = (Closeable) stream;
            try {
              if (closeable != null) {
                closeable.close();
              }
            } catch (IOException ioe) {
              // ignore
            }
          }
          rawPng[0] = stream.toByteArray();
          mainView.destroyDrawingCache();
          done = true;
          syncObject.notify();
        }
      }
    });

    waitForDone(end, serverInstrumentation.getAndroidWait().getTimeoutInMillis(),
        "Failed to take screenshot.");
    return rawPng[0];
  }

  private void waitForDone(long end, long timeout, String error) {
    synchronized (syncObject) {
      while (!done && System.currentTimeMillis() < end) {
        try {
          syncObject.wait(timeout);
        } catch (InterruptedException e) {
          throw new SelendroidException(error, e);
        }
      }
    }
  }

  public void switchContext(String type) {
    Preconditions.checkNotNull(type);
    if (type.equals(activeWindowType)) {
      return;
    } else {
      this.activeWindowType = type;
    }
    if (WindowType.NATIVE_APP.name().equals(type)) {
      this.webviewSearchScope = null;
      this.selendroidWebDriver = null;
    } else {
      initSelendroidWebDriver(type);
    }
    session.getKnownElements().clear();
  }

  private void initSelendroidWebDriver(String type) {
    selendroidWebDriver = new SelendroidWebDriver(serverInstrumentation, type);
    webviewSearchScope =
        new WebviewSearchScope(session.getKnownElements(), selendroidWebDriver.getWebview(),
            selendroidWebDriver);
    selendroidWebDriver.setAsyncScriptTimeout(scriptTimeout);
  }

  /*
   * (non-Javadoc)
   * 
   * @see
   * org.openqa.selenium.android.server.AndroidDriver#initializeSessionForCapabilities(com.google
   * .gson.JsonObject)
   */
  @Override
  public String initializeSession(JSONObject desiredCapabilities) {
    if (this.session != null) {
      session.getKnownElements().clear();
      return session.getSessionId();
    }
    activeWindowType = WindowType.NATIVE_APP.name();
    Random random = new Random();
    this.session =
        new Session(desiredCapabilities, new UUID(random.nextLong(), random.nextLong()).toString());
    nativeSearchScope =
        new NativeSearchScope(serverInstrumentation, getSession().getKnownElements());

    selendroidNativeDriver =
        new SelendroidNativeDriver(serverInstrumentation, (NativeSearchScope) nativeSearchScope);
    SelendroidLogger.info("new s: " + session.getSessionId());

    nativeExecuteScriptMap.put("invokeMenuActionSync", new InvokeMenuAction(session,
        serverInstrumentation));
    nativeExecuteScriptMap.put("findRId", new FindRId(serverInstrumentation));
    nativeExecuteScriptMap.put("getL10nKeyTranslation", new GetL10nKeyTranslation(
        serverInstrumentation));
    nativeExecuteScriptMap.put("findElementByAndroidTag",
        new FindElementByAndroidTag(session.getKnownElements(), serverInstrumentation, keySender));
    nativeExecuteScriptMap.put("isElementDisplayedInViewport", new IsElementDisplayedInViewport(
        session.getKnownElements(), serverInstrumentation));

    return session.getSessionId();
  }

  @Override
  public Keyboard getKeyboard() {
    return keySender.getKeyboard();
  }

  @Override
  public TouchScreen getTouch() {
    if (isNativeWindowMode()) {
      return selendroidNativeDriver.getTouch();
    } else {
      return selendroidWebDriver.getTouch();
    }
  }

  public class WebviewSearchScope extends AbstractWebElementContext {
    public WebviewSearchScope(KnownElements knownElements, WebView webview,
        SelendroidWebDriver driver) {
      super(knownElements, webview, driver);
    }

    @Override
    protected AndroidElement lookupElement(String strategy, String locator) {
      AndroidElement element = null;


      Object result = driver.executeAtom(AndroidAtoms.FIND_ELEMENT, null, strategy, locator);
      if (result == null) {
        return null;
      }
      element = replyElement((JSONObject) result);
      return element;
    }

    @Override
    protected List<AndroidElement> lookupElements(String strategy, String locator) {
      List<AndroidElement> elements = null;

      Object result = driver.executeAtom(AndroidAtoms.FIND_ELEMENTS, null, strategy, locator);
      if (result == null) {
        return new ArrayList<AndroidElement>();
      }
      elements = replyElements((JSONArray) result);
      return elements;
    }
  }

  public class NativeSearchScope extends AbstractNativeElementContext {
    public NativeSearchScope(ServerInstrumentation instrumentation, KnownElements knownElements) {
      super(instrumentation, keySender, knownElements);
    }

    @Override
    protected View getRootView() {
      return viewAnalyzer.getRecentDecorView();
    }

    @Override
    protected View getSearchRoot() {
      return viewAnalyzer.getRecentDecorView();
    }

    @Override
    protected List<View> getTopLevelViews() {
      List<View> views = new ArrayList<View>();
      views.addAll(viewAnalyzer.getTopLevelViews());
      if (instrumentation.getCurrentActivity() != null
          && instrumentation.getCurrentActivity().getCurrentFocus() != null) {
        // Make sure the focused view is not a child of an already added top level view
        View focusedView = instrumentation.getCurrentActivity().getCurrentFocus();
        View focusedRoot = focusedView.getRootView();
        boolean topLevel = true;
        for (View view : views){
          topLevel = topLevel && !focusedRoot.equals(view);
        }
        if (topLevel) views.add(focusedView);
      }
      // sort them to have most recently drawn view show up first
      Collections.sort(views, new Comparator<View>() {
        @Override
        public int compare(View view, View view1) {
          return view.getDrawingTime() < view1.getDrawingTime()
              ? 1
              : (view.getDrawingTime() == view1.getDrawingTime() ? 0 : -1);
        }
      });

      return views;
    }
  }

  @Override
  public String getCurrentUrl() {
    if (isNativeWindowMode()) {
      return selendroidNativeDriver.getCurrentUrl();
    } else {
      return selendroidWebDriver.getCurrentUrl();
    }
  }



  @Override
  public String getWindowSource() {
    try {
      if (isNativeWindowMode()) {
        JSONObject uiTree = selendroidNativeDriver.getWindowSource();
        return TreeUtil.getXMLSource(uiTree);
      } else {
        return selendroidWebDriver.getWindowSource();
      }
    } catch (JSONException e) {
      throw new SelendroidException("Exception while generating source tree.", e);
    }
  }

  @Override
  public JSONObject getFullWindowTree() {
    JSONObject source = null;
    try {
      source = selendroidNativeDriver.getWindowSource();
    } catch (JSONException e) {
      throw new SelendroidException(e);
    }
    return source;
  }

  @Override
  public String getTitle() {
    if (isNativeWindowMode()) {
      return selendroidNativeDriver.getTitle();
    } else {
      return selendroidWebDriver.getTitle();
    }
  }

  @Override
  public void get(String url) {
    if (isNativeWindowMode()) {
      selendroidNativeDriver.get(url);
    } else {
      selendroidWebDriver.get(url);
    }
    getSession().getKnownElements().clear();
  }

  public ScreenOrientation getOrientation() {
    Activity activity = serverInstrumentation.getCurrentActivity();

    int value = activity.getRequestedOrientation();
    if (value == 0) {
      return ScreenOrientation.LANDSCAPE;
    }
    return ScreenOrientation.PORTRAIT;
  }

  public void rotate(final ScreenOrientation orientation) {
    final Activity activity = serverInstrumentation.getCurrentActivity();
    if (activity == null) {
      return;
    }
    final int screenOrientation = getAndroidScreenOrientation(orientation);
    activity.runOnUiThread(new Runnable() {
      @Override
      public void run() {
        activity.setRequestedOrientation(screenOrientation);
      }
    });
    serverInstrumentation.getInstrumentation().waitForIdleSync();
  }

  private int getAndroidScreenOrientation(ScreenOrientation orientation) {
    if (ScreenOrientation.LANDSCAPE.equals(orientation)) {
      return 0;
    }
    return 1;
  }

  public boolean isNativeWindowMode() {
    if (WindowType.NATIVE_APP.name().equals(activeWindowType)) {
      return true;
    }
    return false;
  }

  @Override
  public Object executeScript(String script, JSONArray args) {
    if (isNativeWindowMode()) {
      if (nativeExecuteScriptMap.containsKey(script)) {
        return nativeExecuteScriptMap.get(script).executeScript(args);
      }
      throw new UnsupportedOperationException(
          "Executing arbitrary script is only available in web views.");
    }
    return selendroidWebDriver.executeScript(script, args, session.getKnownElements());
  }

  @Override
  public Object executeScript(String script, Object... args) {
    JSONArray array = new JSONArray();
    for (int i = 0; i < args.length; i++) {
      array.put(args[i]);
    }
    return executeScript(script, array);
  }

  public Object executeAsyncScript(String script, JSONArray args) {
    if (isNativeWindowMode()) {
      throw new UnsupportedOperationException(
          "Executing arbitrary script is only available in web views.");
    }
    return selendroidWebDriver.executeAsyncJavascript(script, args, session.getKnownElements());
  }

  @Override
  public String getContext() {
    if (activeWindowType.equals(WindowType.NATIVE_APP.name())) {
      return activeWindowType;
    }

    return selendroidWebDriver.getContextHandle();
  }

  @Override
  public Dimension getWindowSize() {
    if (isNativeWindowMode()) {
      return selendroidNativeDriver.getWindowSize();
    } else {
      return new Dimension(selendroidWebDriver.getWebview().getWidth(), selendroidWebDriver
          .getWebview().getHeight());
    }
  }

  @Override
  public Set<String> getContexts() {
    Set<String> windowHandles = new HashSet<String>();

    windowHandles.add(WindowType.NATIVE_APP.name());
    windowHandles.addAll(WebViewHandleMapper.webViewHandles());

    return windowHandles;
  }

  @Override
  public void addCookie(String url, Cookie cookie) {

    if (selendroidWebDriver != null) selendroidWebDriver.setCookies(url, cookie);

  }

  @Override
  public Set<Cookie> getCookies(String url) {


    Set<Cookie> coo = new HashSet<Cookie>();
    if (selendroidWebDriver != null) {
      coo.addAll(selendroidWebDriver.getCookies(url));
    }
    return coo;
  }

  @Override
  public void deleteCookie(String url) {

    if (selendroidWebDriver != null) {
      selendroidWebDriver.removeAllCookie(url);
    }
  }

  @Override
  public void deleteNamedCookie(String url, String name) {

    if (selendroidWebDriver != null) {
      selendroidWebDriver.remove(url, name);
    }
  }

  public void setFrameContext(Object obj) throws JSONException {
    if (selendroidWebDriver == null) {
      return;
    }
    SelendroidLogger.info("setting frame context: " + obj);
    if (obj.equals(null)) {
      selendroidWebDriver.switchToDefaultContent();
    } else if (obj instanceof Number) {
      selendroidWebDriver.frame(((Number) obj).intValue());
    } else if (obj instanceof JSONObject && ((JSONObject) obj).has("ELEMENT")) {
      selendroidWebDriver.frame((AndroidWebElement) session.getKnownElements().get(
          ((JSONObject) obj).getString("ELEMENT")));
    } else if (obj instanceof String) {
      selendroidWebDriver.frame((String) obj);
    } else {
      throw new IllegalArgumentException("Unsupported frame locator: " + obj.getClass().getName());
    }
  }

  @Override
  public void back() {
    if (isNativeWindowMode()) {
      getKeyboard().sendKeys("\uE100".split(""));
    } else {
      selendroidWebDriver.back();
    }
  }

  @Override
  public void forward() {
    if (isNativeWindowMode()) {
      selendroidNativeDriver.forward();
    } else {
      selendroidWebDriver.forward();
    }
  }

  @Override
  public void refresh() {
    if (isNativeWindowMode()) {
      selendroidNativeDriver.refresh();
    } else {
      selendroidWebDriver.refresh();
    }
  }


  public boolean isAlertPresent() {
    if (isNativeWindowMode() || selendroidWebDriver == null) {
      // alert handling is not done in 'native' mode
      return false;
    }
    if (selendroidWebDriver.isAlertPresent()) {
      AndroidElement el = findNativeElementWithoutDelay(By.id("button1"));
      return el != null && el.isDisplayed();
    }
    return false;
  }

  public String getAlertText() {
    SelendroidLogger.info("DefaultSelendroidDriver getAlertText");
    return selendroidWebDriver.getCurrentAlertMessage();
  }

  public void acceptAlert() {
    findNativeElementWithoutDelay(By.id("button1")).click();
    selendroidWebDriver.clearCurrentAlertMessage();
  }

  public void dismissAlert() {
    AndroidElement dismiss = findNativeElementWithoutDelay(By.id("button2"));
    if (dismiss != null && dismiss.isDisplayed()) {
      dismiss.click();
      selendroidWebDriver.clearCurrentAlertMessage();
    } else {
      acceptAlert();
    }
  }

  public void setAlertText(CharSequence... keysToSend) {
    findNativeElementWithoutDelay(By.id("value")).enterText(keysToSend);
  }

  private AndroidElement findNativeElementWithoutDelay(By by) {
    long previousTimeout = serverInstrumentation.getAndroidWait().getTimeoutInMillis();
    serverInstrumentation.getAndroidWait().setTimeoutInMillis(0);
    String previousActiveWindow = activeWindowType;
    activeWindowType = WindowType.NATIVE_APP.name();
    try {
      return findElement(by);
    } catch (NoSuchElementException nse) {} finally {
      serverInstrumentation.getAndroidWait().setTimeoutInMillis(previousTimeout);
      activeWindowType = previousActiveWindow;
    }
    return null;
  }

  public void setAsyncTimeout(long timeout) {
    scriptTimeout = timeout;
    if (selendroidWebDriver != null) {
      selendroidWebDriver.setAsyncScriptTimeout(timeout);
    }
  }

  public void setPageLoadTimeout(long timeout) {
    if (selendroidWebDriver != null) {
      selendroidWebDriver.setPageLoadTimeout(timeout);
    }
  }

  public boolean isAirplaneMode() {
    return Settings.System.getInt(
        serverInstrumentation.getCurrentActivity().getContentResolver(),
        Settings.System.AIRPLANE_MODE_ON, 0) == 1;
  }

  public void backgroundApp() {
    serverInstrumentation.backgroundActivity();
  }

  public void resumeApp() {
    serverInstrumentation.resumeActivity();
  }

  public void addCallLog(CallLogEntry log) {
	serverInstrumentation.addCallLog(log);
  }
  
  public List<CallLogEntry> readCallLog() {
    return serverInstrumentation.readCallLog();
  }
  
  private TrackBall getTrackBall() {
	  if (trackBall == null) {
		  trackBall = new AndroidTrackBall(serverInstrumentation);
	  }
	  return trackBall;
  }

  @Override
  public void roll(int dimensionX, int dimensionY) {
    getTrackBall().roll(dimensionX, dimensionY);
  }

}
