/*
 * 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;

import android.Manifest;
import android.app.Activity;
import android.app.Instrumentation;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.os.Handler;
import android.os.Looper;
import android.os.PowerManager;
import android.provider.CallLog;
import android.view.View;
import io.selendroid.server.android.ActivitiesReporter;
import io.selendroid.server.android.AndroidWait;
import io.selendroid.server.common.exceptions.PermissionDeniedException;
import io.selendroid.server.common.exceptions.SelendroidException;
import io.selendroid.server.common.utils.CallLogEntry;
import io.selendroid.server.extension.ExtensionLoader;
import io.selendroid.server.model.ExternalStorage;
import io.selendroid.server.util.Intents;
import io.selendroid.server.util.SelendroidLogger;
import org.json.JSONArray;
import org.json.JSONObject;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;

public class DefaultServerInstrumentation implements ServerInstrumentation {
    public static final int DEFAULT_SERVER_PORT = 8080;
    private Instrumentation instrumentation;
    protected InstrumentationArguments args;
    private AndroidWait androidWait;
    private ActivitiesReporter activitiesReporter;
    protected int serverPort;
    private HttpdThread serverThread;
    protected PowerManager.WakeLock wakeLock;
    private ExtensionLoader extensionLoader;

    public DefaultServerInstrumentation(Instrumentation instrumentation,
                                        InstrumentationArguments args) {
        this.instrumentation = instrumentation;
        this.args = args;

        // Get a wake lock to stop the cpu going to sleep
        PowerManager pm = (PowerManager) instrumentation
          .getContext()
          .getSystemService(Context.POWER_SERVICE);
        this.wakeLock =
          pm.newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK, "Selendroid");

        if (args.isLoadExtensions()) {
            extensionLoader = new ExtensionLoader(instrumentation.getTargetContext(),
                    ExternalStorage.getExtensionDex().getAbsolutePath());
        } else {
            extensionLoader = new ExtensionLoader(instrumentation.getTargetContext());
        }

        activitiesReporter = new ActivitiesReporter();
        androidWait = new AndroidWait();
    }

    @Override
    public void onCreate() {
        SelendroidLogger.info("*** ServerInstrumentation#onCreate() ***");
        serverPort = parseServerPort(args.getServerPort());
        callBeforeApplicationCreateBootstraps();
    }

    @Override
    public void onDestroy() {
        SelendroidLogger.info("*** ServerInstrumentation#onDestroy() ***");
        try {
            if (wakeLock != null) {
                wakeLock.release();
                wakeLock = null;
            }
        } catch (Exception e) {
            SelendroidLogger.error("Error shutting down: ", e);
        }
        stopServer();
    }

    @Override
    public void startService() {
        if (args.getServiceClassName() != null) {
            startService(args.getServiceClassName(), args.getIntentAction());
        }
    }

    @Override
    public void startService(String serviceClassName, String intentAction) {
        SelendroidLogger.info(
          String.format(
            "*** ServerInstrumentation#startService(%s,%s) ***",
            serviceClassName,
            intentAction
          )
        );
        instrumentation.getTargetContext().startService(
                Intents.createStartServiceIntent(instrumentation.getTargetContext(), serviceClassName, intentAction));
    }

    @Override
    public void startMainActivity() {
        doFinishAllActivities();
        if (args.getActivityClassName() != null) {
            startActivity(args.getActivityClassName());
        } else {
            instrumentation
                    .getTargetContext()
                    .startActivity(Intents.createUriIntent(args.getIntentAction(), args.getIntentUri()));
        }
    }

    @Override
    public void startActivity(String activityClassName) {
        SelendroidLogger.info(
          String.format(
            "*** ServerInstrumentation startActivity(%s) ***",
            activityClassName
          )
        );
        doFinishAllActivities();

        // Start the new activity
        Intent intent = Intents.createStartActivityIntent(instrumentation.getTargetContext(), activityClassName);
        instrumentation.getTargetContext().startActivity(intent);
    }

    @Override
    public void startServer() {
        SelendroidLogger.info("*** ServerInstrumentation#startServer() ***");
        // Queue bootstrapping and starting of the main activity on the main thread.
        Handler mainThreadHandler = new Handler();
        mainThreadHandler.post(new Runnable() {
          @Override
          public void run() {
              callAfterApplicationCreateBootstraps();
              if (args.getServiceClassName() != null) {
                  startService();
              } else {
                  startMainActivity();
              }
              try {
                  startServerImpl();
              } catch (Exception e) {
                  SelendroidLogger.error("Failed to start Selendroid server", e);
              }
          }
        });
    }

    // TODO: Make this class a base class and extract all the serverthread
    // specific logic to a subclass
    protected void startServerImpl() {
        SelendroidLogger.info("*** ServerInstrumentation#startServerImpl() ***");
        if (serverThread != null && serverThread.isAlive()) {
            return;
        }

        if (serverThread != null) {
            stopServer();
        }

        serverThread = new HttpdThread(this, serverPort);
        serverThread.startServer();
    }

    @Override
    public void stopServer() {
        SelendroidLogger.info("*** ServerInstrumentation#stopServer() ***");
        if (serverThread == null) {
            return;
        }
        if (!serverThread.isAlive()) {
            serverThread = null;
            return;
        }

        SelendroidLogger.info("Stopping selendroid http server");
        serverThread.stopLooping();
        serverThread.interrupt();
        try {
            serverThread.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        serverThread = null;
    }

    @Override
    public View getRootView() {
        try {
            View decorView = getCurrentActivity().getWindow().getDecorView();
            if (decorView != null) {
                View rootView = null;// decorView.findViewById(android.R.id.content);
                if (rootView != null) {
                    return rootView;
                }
            }
            return decorView;
        } catch (Exception e) {
            SelendroidLogger.error("Error searching for root view: ", e);
        }

        throw new SelendroidException("Could not find any views");
    }

    @Override
    public ActivitiesReporter getActivitiesReporter() {
        return activitiesReporter;
    }

    @Override
    public Activity getCurrentActivity() {
        return activitiesReporter.getCurrentActivity();
    }

    @Override
    public void setImplicitWait(long millis) {
        androidWait.setTimeoutInMillis(millis);
    }

    @Override
    public void finishAllActivities() {
        instrumentation.runOnMainSync(new Runnable() {
            @Override
            public void run() {
                doFinishAllActivities();
            }
        });
    }

    @Override
    public AndroidWait getAndroidWait() {
        return androidWait;
    }

    @Override
    public JSONObject getExtraArgs() {
      return args.getExtraArgs();
    }

    @Override
    public Object getExtraArg(String key) {
      return args.getExtraArg(key);
    }

    @Override
    public void backgroundActivity() {
        activitiesReporter.setBackgroundActivity(activitiesReporter.getCurrentActivity());
        Intent homeIntent = new Intent(Intent.ACTION_MAIN);
        homeIntent.addCategory(Intent.CATEGORY_HOME);
        homeIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        instrumentation.getTargetContext().startActivity(homeIntent);
    }

    @Override
    public void resumeActivity() {
        Activity activity = activitiesReporter.getBackgroundActivity();
        SelendroidLogger.info("got background activity");
        if (activity == null) {
            SelendroidLogger
                    .error("activity class is empty", new NullPointerException(
                            "Activity class to start is null."));
            return;
        }
        // start now the new activity
        SelendroidLogger.info("background activity is not null");
        Intent intent = new Intent(instrumentation.getTargetContext(), activity.getClass());
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
                | Intent.FLAG_ACTIVITY_REORDER_TO_FRONT
                | Intent.FLAG_ACTIVITY_SINGLE_TOP
                | Intent.FLAG_ACTIVITY_CLEAR_TOP);
        SelendroidLogger.info("created intent and got target context");
        instrumentation.getTargetContext().startActivity(intent);
        SelendroidLogger.info("got target context and started activity");
        activitiesReporter.setBackgroundActivity(null);
    }

    @Override
    public void addCallLog(CallLogEntry log) {
        String permission = Manifest.permission.WRITE_CALL_LOG;
        if (instrumentation.getTargetContext().checkCallingOrSelfPermission(permission) == PackageManager.PERMISSION_GRANTED) {
            ContentValues values = new ContentValues();
            values.put(CallLog.Calls.CACHED_NUMBER_TYPE, 0);
            values.put(CallLog.Calls.TYPE, log.getDirection());
            values.put(CallLog.Calls.DATE, log.getDate().getTime());
            values.put(CallLog.Calls.DURATION, log.getDuration());
            values.put(CallLog.Calls.NUMBER, log.getNumber());
            instrumentation.getTargetContext().getContentResolver().insert(CallLog.Calls.CONTENT_URI, values);
        } else {
            throw new PermissionDeniedException("Application Under Test does not have the required WRITE_CALL_LOGS permission for this feature..");
        }
    }

    @Override
    public List<CallLogEntry> readCallLog() {
        if (instrumentation.getTargetContext().checkCallingOrSelfPermission(Manifest.permission.READ_CALL_LOG) == PackageManager.PERMISSION_GRANTED) {
            List<CallLogEntry> logs = new ArrayList<CallLogEntry>();
            Cursor managedCursor = instrumentation.getTargetContext().getContentResolver().query(CallLog.Calls.CONTENT_URI, null, null, null, null);
            int number = managedCursor.getColumnIndex(CallLog.Calls.NUMBER);
            int type = managedCursor.getColumnIndex(CallLog.Calls.TYPE);
            int date = managedCursor.getColumnIndex(CallLog.Calls.DATE);
            int duration = managedCursor.getColumnIndex(CallLog.Calls.DURATION);
            while (managedCursor.moveToNext()) {
                String phNumber = managedCursor.getString(number);
                String callType = managedCursor.getString(type);
                String callDate = managedCursor.getString(date);
                Date callDayTime = new Date(Long.valueOf(callDate));
                String callDuration = managedCursor.getString(duration);
                logs.add(new CallLogEntry(phNumber, Integer.parseInt(callDuration), callDayTime, Integer.parseInt(callType)));
            }
            managedCursor.close();
            return logs;
        } else {
            throw new PermissionDeniedException("Application under test does not have required READ_CALL_LOG permission for this feature.");
        }

    }

    @Override
    public Instrumentation getInstrumentation() {
        return instrumentation;
    }

    @Override
    public ExtensionLoader getExtensionLoader() {
        return extensionLoader;
    }

    @Override
    public String getServerVersion() {
        Context context = instrumentation.getContext();
        String versionName = "0.3";
        try {
            versionName =
                    context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionName;
        } catch (PackageManager.NameNotFoundException e) {
        }
        return versionName;
    }

    @Override
    public String getCpuArch() {
        return android.os.Build.CPU_ABI;
    }

    @Override
    public String getOsName() {
        return "Android";
    }

    @Override
    public String getOsVersion() {
        return String.valueOf(android.os.Build.VERSION.SDK_INT);
    }

    @Override
    public JSONArray getSupportedApps() {
        return new JSONArray();
    }

    @Override
    public JSONArray getSupportedDevices() {
        return new JSONArray();
    }

    private void doFinishAllActivities() {
        Set<Activity> activities = activitiesReporter.getActivities();
        if (activities != null && !activities.isEmpty()) {
            for (Activity activity : activities) {
                activity.finish();
            }
        }
    }

    public void callBeforeApplicationCreateBootstraps() {
        if (!args.isLoadExtensions() || args.getBootstrapClassNames() == null) {
            return;
        }
        extensionLoader.runBeforeApplicationCreateBootstrap(instrumentation, args.getBootstrapClassNames().split(","));
    }

    public void callAfterApplicationCreateBootstraps() {
        if (!args.isLoadExtensions() || args.getBootstrapClassNames() == null) {
            return;
        }
        extensionLoader.runAfterApplicationCreateBootstrap(instrumentation, args.getBootstrapClassNames().split(","));
    }

    private class HttpdThread extends Thread {

        private final AndroidServer server;
        private ServerInstrumentation instrumentation = null;
        private Looper looper;

        public HttpdThread(ServerInstrumentation instrumentation, int serverPort) {
            this.instrumentation = instrumentation;
            // Create the server but absolutely do not start it here
            server = new AndroidServer(this.instrumentation, serverPort);
        }

        @Override
        public void run() {
            Looper.prepare();
            looper = Looper.myLooper();
            startServer();
            Looper.loop();
        }

        public AndroidServer getServer() {
            return server;
        }

        private void startServer() {
            try {
              startAndroidServer(
                this.server,
                DefaultServerInstrumentation.this.wakeLock);
            } catch (Exception e) {
                SelendroidLogger.error("Error starting httpd.", e);

                throw new SelendroidException("Httpd failed to start!");
            }
        }

        public void stopLooping() {
            if (looper == null) {
                return;
            }
            looper.quit();
        }
    }

    protected int parseServerPort(String port) {
        int parsedServerPort;
        try {
                parsedServerPort = Integer.parseInt(port);
            } catch (NumberFormatException e) {
                SelendroidLogger.info("Failed to parse server port, defaulting to 8080");
                parsedServerPort = DEFAULT_SERVER_PORT;
            }
                if (!isValidPort(parsedServerPort)) {
            SelendroidLogger.info("Invalid port " + parsedServerPort + ", defaulting to " + DEFAULT_SERVER_PORT);
        }
        return parsedServerPort;
    }

    protected static void startAndroidServer(
      AndroidServer server,
      PowerManager.WakeLock wakeLock
    ) {
      try {
          wakeLock.acquire();
      } catch (SecurityException e) {
      }

      server.start();

      SelendroidLogger.info("Started selendroid http server on port " + server.getPort());
    }

    private boolean isValidPort(int port) {
        return port >= 1024 && port <= 65535;
    }
}
