/*
 * Copyright (C) 2008 The Android Open Source Project
 *
 * 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 com.android.hierarchyviewer.device;

import com.android.ddmlib.AdbCommandRejectedException;
import com.android.ddmlib.AndroidDebugBridge;
import com.android.ddmlib.IDevice;
import com.android.ddmlib.Log;
import com.android.ddmlib.MultiLineReceiver;
import com.android.ddmlib.ShellCommandUnresponsiveException;
import com.android.ddmlib.TimeoutException;
import com.android.hierarchyviewer.scene.VersionLoader;

import java.io.IOException;
import java.io.File;
import java.util.HashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class DeviceBridge {
    private static AndroidDebugBridge bridge;

    private static final HashMap<IDevice, Integer> devicePortMap = new HashMap<IDevice, Integer>();
    private static int nextLocalPort = Configuration.DEFAULT_SERVER_PORT;

    public static void initDebugBridge() {
        if (bridge == null) {
            AndroidDebugBridge.init(false /* debugger support */);
        }
        if (bridge == null || !bridge.isConnected()) {
            String adbLocation = System.getProperty("hierarchyviewer.adb");
            if (adbLocation != null && adbLocation.length() != 0) {
                adbLocation += File.separator + "adb";
            } else {
                adbLocation = "adb";
            }

            bridge = AndroidDebugBridge.createBridge(adbLocation, true);
        }
    }

    public static void startListenForDevices(AndroidDebugBridge.IDeviceChangeListener listener) {
        AndroidDebugBridge.addDeviceChangeListener(listener);
    }

    public static IDevice[] getDevices() {
        return bridge.getDevices();
    }

    public static boolean isViewServerRunning(IDevice device) {
        initDebugBridge();
        final boolean[] result = new boolean[1];
        try {
            if (device.isOnline()) {
                device.executeShellCommand(buildIsServerRunningShellCommand(),
                        new BooleanResultReader(result));
                if (!result[0]) {
                    if (VersionLoader.loadProtocolVersion(device) > 2) {
                        result[0] = true;
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } catch (TimeoutException e) {
            e.printStackTrace();
        } catch (AdbCommandRejectedException e) {
            e.printStackTrace();
        } catch (ShellCommandUnresponsiveException e) {
            e.printStackTrace();
        }
        return result[0];
    }

    public static boolean startViewServer(IDevice device) {
        return startViewServer(device, Configuration.DEFAULT_SERVER_PORT);
    }

    public static boolean startViewServer(IDevice device, int port) {
        initDebugBridge();
        final boolean[] result = new boolean[1];
        try {
            if (device.isOnline()) {
                device.executeShellCommand(buildStartServerShellCommand(port),
                        new BooleanResultReader(result));
            }
        } catch (IOException e) {
            e.printStackTrace();
        } catch (TimeoutException e) {
            e.printStackTrace();
        } catch (AdbCommandRejectedException e) {
            e.printStackTrace();
        } catch (ShellCommandUnresponsiveException e) {
            e.printStackTrace();
        }
        return result[0];
    }

    public static boolean stopViewServer(IDevice device) {
        initDebugBridge();
        final boolean[] result = new boolean[1];
        try {
            if (device.isOnline()) {
                device.executeShellCommand(buildStopServerShellCommand(),
                        new BooleanResultReader(result));
            }
        } catch (IOException e) {
            e.printStackTrace();
        } catch (TimeoutException e) {
            e.printStackTrace();
        } catch (AdbCommandRejectedException e) {
            e.printStackTrace();
        } catch (ShellCommandUnresponsiveException e) {
            e.printStackTrace();
        }
        return result[0];
    }

    public static void terminate() {
        AndroidDebugBridge.terminate();
    }

    /**
     * Sets up a just-connected device to work with the view server.
     * <p/>This starts a port forwarding between a local port and a port on the device.
     * @param device
     */
    public static void setupDeviceForward(IDevice device) {
        synchronized (devicePortMap) {
            if (device.getState() == IDevice.DeviceState.ONLINE) {
                int localPort = nextLocalPort++;
                try {
                    device.createForward(localPort, Configuration.DEFAULT_SERVER_PORT);
                    devicePortMap.put(device, localPort);
                } catch (TimeoutException e) {
                    Log.e("hierarchy", "Timeout setting up port forwarding for " + device);
                } catch (AdbCommandRejectedException e) {
                    Log.e("hierarchy", String.format(
                            "Adb rejected forward command for device %1$s: %2$s",
                            device, e.getMessage()));
                } catch (IOException e) {
                    Log.e("hierarchy", String.format(
                            "Failed to create forward for device %1$s: %2$s",
                            device, e.getMessage()));
                }
            }
        }
    }

    public static void removeDeviceForward(IDevice device) {
        synchronized (devicePortMap) {
            final Integer localPort = devicePortMap.get(device);
            if (localPort != null) {
                try {
                    device.removeForward(localPort, Configuration.DEFAULT_SERVER_PORT);
                    devicePortMap.remove(device);
                } catch (TimeoutException e) {
                    Log.e("hierarchy", "Timeout removing port forwarding for " + device);
                } catch (AdbCommandRejectedException e) {
                    Log.e("hierarchy", String.format(
                            "Adb rejected remove-forward command for device %1$s: %2$s",
                            device, e.getMessage()));
                } catch (IOException e) {
                    Log.e("hierarchy", String.format(
                            "Failed to remove forward for device %1$s: %2$s",
                            device, e.getMessage()));
                }
            }
        }
    }

    public static int getDeviceLocalPort(IDevice device) {
        synchronized (devicePortMap) {
            Integer port = devicePortMap.get(device);
            if (port != null) {
                return port;
            }

            Log.e("hierarchy", "Missing forwarded port for " + device.getSerialNumber());
            return -1;
        }

    }

    private static String buildStartServerShellCommand(int port) {
        return String.format("service call window %d i32 %d",
                Configuration.SERVICE_CODE_START_SERVER, port);
    }

    private static String buildStopServerShellCommand() {
        return String.format("service call window %d", Configuration.SERVICE_CODE_STOP_SERVER);
    }

    private static String buildIsServerRunningShellCommand() {
        return String.format("service call window %d",
                Configuration.SERVICE_CODE_IS_SERVER_RUNNING);
    }

    private static class BooleanResultReader extends MultiLineReceiver {
        private final boolean[] mResult;

        public BooleanResultReader(boolean[] result) {
            mResult = result;
        }

        @Override
        public void processNewLines(String[] strings) {
            if (strings.length > 0) {
                Pattern pattern = Pattern.compile(".*?\\([0-9]{8} ([0-9]{8}).*");
                Matcher matcher = pattern.matcher(strings[0]);
                if (matcher.matches()) {
                    if (Integer.parseInt(matcher.group(1)) == 1) {
                        mResult[0] = true;
                    }
                }
            }
        }

        public boolean isCancelled() {
            return false;
        }
    }
}
