package uixt

import (
	"bytes"
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"net"
	"net/url"
	"strconv"
	"strings"
	"time"

	"github.com/electricbubble/gadb"
	"github.com/rs/zerolog/log"
)

var errDriverNotImplemented = errors.New("driver method not implemented")

type uiaDriver struct {
	Driver

	adbDevice gadb.Device
	localPort int
}

func NewUIADriver(capabilities Capabilities, urlPrefix string) (driver *uiaDriver, err error) {
	if capabilities == nil {
		capabilities = NewCapabilities()
	}
	driver = new(uiaDriver)
	if driver.urlPrefix, err = url.Parse(urlPrefix); err != nil {
		return nil, err
	}
	var localPort int
	{
		tmpURL, _ := url.Parse(driver.urlPrefix.String())
		hostname := tmpURL.Hostname()
		if strings.HasPrefix(hostname, forwardToPrefix) {
			localPort, _ = strconv.Atoi(strings.TrimPrefix(hostname, forwardToPrefix))
		}
	}
	conn, err := net.Dial("tcp", fmt.Sprintf(":%d", localPort))
	if err != nil {
		return nil, fmt.Errorf("adb forward: %w", err)
	}
	driver.client = convertToHTTPClient(conn)
	if session, err := driver.NewSession(capabilities); err != nil {
		return nil, err
	} else {
		driver.sessionId = session.SessionId
	}
	return
}

type BatteryStatus int

const (
	_                                  = iota
	BatteryStatusUnknown BatteryStatus = iota
	BatteryStatusCharging
	BatteryStatusDischarging
	BatteryStatusNotCharging
	BatteryStatusFull
)

func (bs BatteryStatus) String() string {
	switch bs {
	case BatteryStatusUnknown:
		return "unknown"
	case BatteryStatusCharging:
		return "charging"
	case BatteryStatusDischarging:
		return "discharging"
	case BatteryStatusNotCharging:
		return "not charging"
	case BatteryStatusFull:
		return "full"
	default:
		return fmt.Sprintf("unknown status code (%d)", bs)
	}
}

func (ud *uiaDriver) Close() (err error) {
	if ud.sessionId == "" {
		return nil
	}
	if _, err = ud.httpDELETE("/session", ud.sessionId); err == nil {
		ud.sessionId = ""
	}

	return err
}

func (ud *uiaDriver) NewSession(capabilities Capabilities) (sessionInfo SessionInfo, err error) {
	// register(postHandler, new NewSession("/wd/hub/session"))
	var rawResp rawResponse
	data := map[string]interface{}{"capabilities": capabilities}
	if rawResp, err = ud.httpPOST(data, "/session"); err != nil {
		return SessionInfo{SessionId: ""}, err
	}
	reply := new(struct{ Value struct{ SessionId string } })
	if err = json.Unmarshal(rawResp, reply); err != nil {
		return SessionInfo{SessionId: ""}, err
	}
	sessionID := reply.Value.SessionId
	// d.sessionIdCache[sessionID] = true
	return SessionInfo{SessionId: sessionID}, nil
}

func (ud *uiaDriver) ActiveSession() (sessionInfo SessionInfo, err error) {
	// [[FBRoute GET:@""] respondWithTarget:self action:@selector(handleGetActiveSession:)]
	return SessionInfo{SessionId: ud.sessionId}, nil
}

func (ud *uiaDriver) SessionIDs() (sessionIDs []string, err error) {
	// register(getHandler, new GetSessions("/wd/hub/sessions"))
	var rawResp rawResponse
	if rawResp, err = ud.httpGET("/sessions"); err != nil {
		return nil, err
	}
	reply := new(struct{ Value []struct{ SessionId string } })
	if err = json.Unmarshal(rawResp, reply); err != nil {
		return nil, err
	}

	sessionIDs = make([]string, len(reply.Value))
	for i := range reply.Value {
		sessionIDs[i] = reply.Value[i].SessionId
	}
	return
}

func (ud *uiaDriver) SessionDetails() (scrollData map[string]interface{}, err error) {
	// register(getHandler, new GetSessionDetails("/wd/hub/session/:sessionId"))
	var rawResp rawResponse
	if rawResp, err = ud.httpGET("/session", ud.sessionId); err != nil {
		return nil, err
	}
	var reply = new(struct{ Value map[string]interface{} })
	if err = json.Unmarshal(rawResp, reply); err != nil {
		return nil, err
	}

	scrollData = reply.Value
	return
}

func (ud *uiaDriver) DeleteSession() (err error) {
	// TODO
	return errDriverNotImplemented
}

func (ud *uiaDriver) Status() (deviceStatus DeviceStatus, err error) {
	// register(getHandler, new Status("/wd/hub/status"))
	var rawResp rawResponse
	if rawResp, err = ud.httpGET("/status"); err != nil {
		return DeviceStatus{Ready: false}, err
	}
	reply := new(struct {
		Value struct {
			// Message string
			Ready bool
		}
	})
	if err = json.Unmarshal(rawResp, reply); err != nil {
		return DeviceStatus{Ready: false}, err
	}
	return DeviceStatus{Ready: true}, nil
}

func (ud *uiaDriver) DeviceInfo() (deviceInfo DeviceInfo, err error) {
	// register(getHandler, new GetDeviceInfo("/wd/hub/session/:sessionId/appium/device/info"))
	var rawResp rawResponse
	if rawResp, err = ud.httpGET("/session", ud.sessionId, "appium/device/info"); err != nil {
		return DeviceInfo{}, err
	}
	reply := new(struct{ Value struct{ DeviceInfo } })
	if err = json.Unmarshal(rawResp, reply); err != nil {
		return DeviceInfo{}, err
	}
	deviceInfo = reply.Value.DeviceInfo
	return
}

func (ud *uiaDriver) Location() (location Location, err error) {
	// TODO
	return location, errDriverNotImplemented
}

func (ud *uiaDriver) BatteryInfo() (batteryInfo BatteryInfo, err error) {
	// register(getHandler, new GetBatteryInfo("/wd/hub/session/:sessionId/appium/device/battery_info"))
	var rawResp rawResponse
	if rawResp, err = ud.httpGET("/session", ud.sessionId, "appium/device/battery_info"); err != nil {
		return BatteryInfo{}, err
	}
	reply := new(struct{ Value struct{ BatteryInfo } })
	if err = json.Unmarshal(rawResp, reply); err != nil {
		return BatteryInfo{}, err
	}
	if reply.Value.Level == -1 || reply.Value.Status == -1 {
		return reply.Value.BatteryInfo, errors.New("cannot be retrieved from the system")
	}
	batteryInfo = reply.Value.BatteryInfo
	return
}

func (ud *uiaDriver) WindowSize() (size Size, err error) {
	// register(getHandler, new GetDeviceSize("/wd/hub/session/:sessionId/window/:windowHandle/size"))
	var rawResp rawResponse
	if rawResp, err = ud.httpGET("/session", ud.sessionId, "window/:windowHandle/size"); err != nil {
		return Size{}, err
	}
	reply := new(struct{ Value struct{ Size } })
	if err = json.Unmarshal(rawResp, reply); err != nil {
		return Size{}, err
	}
	size = reply.Value.Size
	return
}

func (ud *uiaDriver) Screen() (screen Screen, err error) {
	// TODO
	return screen, errDriverNotImplemented
}

func (ud *uiaDriver) Scale() (scale float64, err error) {
	return 1, nil
}

// PressBack simulates a short press on the BACK button.
func (ud *uiaDriver) PressBack() (err error) {
	// register(postHandler, new PressBack("/wd/hub/session/:sessionId/back"))
	_, err = ud.httpPOST(nil, "/session", ud.sessionId, "back")
	return
}

func (ud *uiaDriver) StartCamera() (err error) {
	if _, err = ud.adbDevice.RunShellCommand("am", "start", "-a", "android.media.action.VIDEO_CAPTURE"); err != nil {
		return err
	}
	return
}

func (ud *uiaDriver) StopCamera() (err error) {
	err = ud.PressBack()
	if err != nil {
		return err
	}
	err = ud.Homescreen()
	if err != nil {
		return err
	}

	// kill samsung shell command
	if _, err = ud.adbDevice.RunShellCommand("am", "force-stop", "com.sec.android.app.camera"); err != nil {
		return err
	}

	// kill other camera (huawei mi)
	if _, err = ud.adbDevice.RunShellCommand("am", "force-stop", "com.android.camera2"); err != nil {
		return err
	}
	return
}

func (ud *uiaDriver) StartRecording() (err error) {
	var res string
	if res, err = ud.adbDevice.RunShellCommand("input", "keyevent", "27"); err != nil {
		return err
	}
	log.Info().Str("shell", res)
	return
}

func (ud *uiaDriver) StopRecording() (err error) {
	var res string
	if res, err = ud.adbDevice.RunShellCommand("input", "keyevent", "27"); err != nil {
		return err
	}
	log.Info().Str("shell", res)
	return
}

func (ud *uiaDriver) ActiveAppInfo() (info AppInfo, err error) {
	// TODO
	return info, errDriverNotImplemented
}

func (ud *uiaDriver) ActiveAppsList() (appsList []AppBaseInfo, err error) {
	// TODO
	return appsList, errDriverNotImplemented
}

func (ud *uiaDriver) AppState(bundleId string) (runState AppState, err error) {
	// TODO
	return runState, errDriverNotImplemented
}

func (ud *uiaDriver) IsLocked() (locked bool, err error) {
	// TODO
	return locked, errDriverNotImplemented
}

func (ud *uiaDriver) Unlock() (err error) {
	// TODO
	return errDriverNotImplemented
}

func (ud *uiaDriver) Lock() (err error) {
	// TODO
	return errDriverNotImplemented
}

func (ud *uiaDriver) Homescreen() (err error) {
	return ud.PressKeyCode(KCHome, KMEmpty)
}

func (ud *uiaDriver) PressKeyCode(keyCode KeyCode, metaState KeyMeta, flags ...KeyFlag) (err error) {
	if len(flags) == 0 {
		flags = []KeyFlag{KFFromSystem}
	}
	return ud._pressKeyCode(keyCode, metaState, KFFromSystem)
}

func (ud *uiaDriver) _pressKeyCode(keyCode KeyCode, metaState KeyMeta, flags ...KeyFlag) (err error) {
	// register(postHandler, new PressKeyCodeAsync("/wd/hub/session/:sessionId/appium/device/press_keycode"))
	data := map[string]interface{}{
		"keycode": keyCode,
	}
	if metaState != KMEmpty {
		data["metastate"] = metaState
	}
	if len(flags) != 0 {
		data["flags"] = flags[0]
	}
	_, err = ud.httpPOST(data, "/session", ud.sessionId, "appium/device/press_keycode")
	return
}

func (ud *uiaDriver) AlertText() (text string, err error) {
	// register(getHandler, new GetAlertText("/wd/hub/session/:sessionId/alert/text"))
	var rawResp rawResponse
	if rawResp, err = ud.httpGET("/session", ud.sessionId, "alert/text"); err != nil {
		return "", err
	}
	reply := new(struct{ Value string })
	if err = json.Unmarshal(rawResp, reply); err != nil {
		return "", err
	}

	text = reply.Value
	return
}

func (ud *uiaDriver) AlertButtons() (btnLabels []string, err error) {
	// TODO
	return btnLabels, errDriverNotImplemented
}

func (ud *uiaDriver) AlertAccept(label ...string) (err error) {
	data := map[string]interface{}{
		"buttonLabel": nil,
	}
	if len(label) != 0 {
		data["buttonLabel"] = label[0]
	}
	// register(postHandler, new AcceptAlert("/wd/hub/session/:sessionId/alert/accept"))
	_, err = ud.httpPOST(data, "/session", ud.sessionId, "alert/accept")
	return
}

func (ud *uiaDriver) AlertDismiss(label ...string) (err error) {
	data := map[string]interface{}{
		"buttonLabel": nil,
	}
	if len(label) != 0 {
		data["buttonLabel"] = label[0]
	}
	// register(postHandler, new DismissAlert("/wd/hub/session/:sessionId/alert/dismiss"))
	_, err = ud.httpPOST(data, "/session", ud.sessionId, "alert/dismiss")
	return
}

func (ud *uiaDriver) AlertSendKeys(text string) (err error) {
	// TODO
	return errDriverNotImplemented
}

func (ud *uiaDriver) check() error {
	if ud.adbDevice.Serial() == "" {
		return errors.New("adb daemon: the device is not ready")
	}
	return nil
}

func (ud *uiaDriver) AppLaunch(bundleId string, launchOpt ...AppLaunchOption) (err error) {
	if err = ud.check(); err != nil {
		return err
	}

	var sOutput string
	if sOutput, err = ud.adbDevice.RunShellCommand("monkey -p", bundleId, "-c android.intent.category.LAUNCHER 1"); err != nil {
		return err
	}
	if strings.Contains(sOutput, "monkey aborted") {
		return fmt.Errorf("app launch: %s", strings.TrimSpace(sOutput))
	}

	if len(launchOpt) != 0 {
		var ce error
		exists := func(ud WebDriver) (bool, error) {
			for _, opt := range launchOpt {
				if waitForComplete, ok := opt["androidBySelector"]; ok {
					for _, e := range waitForComplete.([]BySelector) {
						_, ce = ud.FindElement(e)
						if ce == nil {
							return true, nil
						}
					}
				}
			}
			return false, nil
		}
		if err = ud.WaitWithTimeoutAndInterval(exists, 45, 1); err != nil {
			return fmt.Errorf("app launch (waitForComplete): %s: %w", err.Error(), ce)
		}
	}
	return
}

func (ud *uiaDriver) AppLaunchUnattached(bundleId string) (err error) {
	// TODO
	return errDriverNotImplemented
}

// Dispose corresponds to the command:
//  adb -s $serial forward --remove $localPort
func (ud *uiaDriver) Dispose() (err error) {
	if err = ud.check(); err != nil {
		return err
	}
	if ud.localPort == 0 {
		return nil
	}
	return ud.adbDevice.ForwardKill(ud.localPort)
}

func (ud *uiaDriver) AppTerminate(bundleId string) (successful bool, err error) {
	if err = ud.check(); err != nil {
		return false, err
	}

	_, err = ud.adbDevice.RunShellCommand("am force-stop", bundleId)
	return err == nil, err
}

func (ud *uiaDriver) AppActivate(bundleId string) (err error) {
	// TODO
	return errDriverNotImplemented
}

func (ud *uiaDriver) AppDeactivate(second float64) (err error) {
	// TODO
	return errDriverNotImplemented
}

func (ud *uiaDriver) AppAuthReset(resource ProtectedResource) (err error) {
	// TODO
	return errDriverNotImplemented
}

func (ud *uiaDriver) Tap(x, y int, options ...DataOption) error {
	return ud.TapFloat(float64(x), float64(y))
}

func (ud *uiaDriver) TapFloat(x, y float64, options ...DataOption) (err error) {
	// register(postHandler, new Tap("/wd/hub/session/:sessionId/appium/tap"))
	data := map[string]interface{}{
		"x": x,
		"y": y,
	}
	_, err = ud.httpPOST(data, "/session", ud.sessionId, "appium/tap")
	return
}

func (ud *uiaDriver) DoubleTap(x, y int) error {
	return ud.DoubleTapFloat(float64(x), float64(y))
}

func (ud *uiaDriver) DoubleTapFloat(x, y float64) (err error) {
	// TODO
	return errDriverNotImplemented
}

func (ud *uiaDriver) TouchAndHold(x, y int, second ...float64) (err error) {
	return ud.TouchAndHoldFloat(float64(x), float64(y), second...)
}

func (ud *uiaDriver) TouchAndHoldFloat(x, y float64, second ...float64) (err error) {
	if len(second) == 0 {
		second = []float64{1.0}
	}
	// register(postHandler, new TouchLongClick("/wd/hub/session/:sessionId/touch/longclick"))
	data := map[string]interface{}{
		"params": map[string]interface{}{
			"x":        x,
			"y":        y,
			"duration": int(second[0] * 1000),
		},
	}
	_, err = ud.httpPOST(data, "/session", ud.sessionId, "touch/longclick")
	return
}

func (ud *uiaDriver) _drag(data map[string]interface{}) (err error) {
	// register(postHandler, new Drag("/wd/hub/session/:sessionId/touch/drag"))
	_, err = ud.httpPOST(data, "/session", ud.sessionId, "touch/drag")
	return
}

// Drag performs a swipe from one coordinate to another coordinate. You can control
// the smoothness and speed of the swipe by specifying the number of steps.
// Each step execution is throttled to 5 milliseconds per step, so for a 100
// steps, the swipe will take around 0.5 seconds to complete.
func (ud *uiaDriver) Drag(fromX, fromY, toX, toY int, options ...DataOption) error {
	return ud.DragFloat(float64(fromX), float64(fromY), float64(toX), float64(toY), options...)
}

func (ud *uiaDriver) DragFloat(fromX, fromY, toX, toY float64, options ...DataOption) (err error) {
	data := map[string]interface{}{
		"startX": fromX,
		"startY": fromY,
		"endX":   toX,
		"endY":   toY,
	}

	// append options in post data for extra WDA configurations
	// e.g. use WithPressDuration to set pressForDuration
	for _, option := range options {
		option(data)
	}

	if _, ok := data["steps"]; !ok {
		data["steps"] = 12 // default steps
	}

	return ud._drag(data)
}

func (ud *uiaDriver) _swipe(startX, startY, endX, endY interface{}, steps int, elementID ...string) (err error) {
	// register(postHandler, new Swipe("/wd/hub/session/:sessionId/touch/perform"))
	data := map[string]interface{}{
		"startX": startX,
		"startY": startY,
		"endX":   endX,
		"endY":   endY,
		"steps":  steps,
	}
	if len(elementID) != 0 {
		data["elementId"] = elementID[0]
	}
	_, err = ud.httpPOST(data, "/session", ud.sessionId, "touch/perform")
	return
}

// Swipe performs a swipe from one coordinate to another using the number of steps
// to determine smoothness and speed. Each step execution is throttled to 5ms
// per step. So for a 100 steps, the swipe will take about 1/2 second to complete.
//  `steps` is the number of move steps sent to the system
func (ud *uiaDriver) Swipe(fromX, fromY, toX, toY int, options ...DataOption) error {
	options = append(options, WithPressDuration(0))
	return ud.SwipeFloat(float64(fromX), float64(fromY), float64(toX), float64(toY), options...)
}

func (ud *uiaDriver) SwipeFloat(fromX, fromY, toX, toY float64, options ...DataOption) error {
	data := map[string]interface{}{}
	// append options in post data for extra WDA configurations
	// e.g. use WithPressDuration to set pressForDuration
	for _, option := range options {
		option(data)
	}

	if _, ok := data["steps"]; !ok {
		data["steps"] = 12 // default steps
	}

	return ud._swipe(fromX, fromY, toX, toY, data["steps"].(int))
}

func (ud *uiaDriver) ForceTouch(x, y int, pressure float64, second ...float64) error {
	return ud.ForceTouchFloat(float64(x), float64(y), pressure, second...)
}

func (ud *uiaDriver) ForceTouchFloat(x, y, pressure float64, second ...float64) (err error) {
	// TODO
	return errDriverNotImplemented
}

func (ud *uiaDriver) PerformW3CActions(actions *W3CActions) (err error) {
	data := map[string]interface{}{
		"actions": actions,
	}
	// register(postHandler, new W3CActions("/wd/hub/session/:sessionId/actions"))
	_, err = ud.httpPOST(data, "/session", ud.sessionId, "/actions")
	return
}

func (ud *uiaDriver) PerformAppiumTouchActions(touchActs *TouchActions) (err error) {
	// TODO
	return errDriverNotImplemented
}

func (ud *uiaDriver) SetPasteboard(contentType PasteboardType, content string) (err error) {
	lbl := content

	const defaultLabelLen = 10
	if len(lbl) > defaultLabelLen {
		lbl = lbl[:defaultLabelLen]
	}

	data := map[string]interface{}{
		"contentType": contentType,
		"label":       lbl,
		"content":     base64.StdEncoding.EncodeToString([]byte(content)),
	}
	// register(postHandler, new SetClipboard("/wd/hub/session/:sessionId/appium/device/set_clipboard"))
	_, err = ud.httpPOST(data, "/session", ud.sessionId, "appium/device/set_clipboard")
	return
}

func (ud *uiaDriver) GetPasteboard(contentType PasteboardType) (raw *bytes.Buffer, err error) {
	if len(contentType) == 0 {
		contentType = PasteboardTypePlaintext
	}
	// register(postHandler, new GetClipboard("/wd/hub/session/:sessionId/appium/device/get_clipboard"))
	data := map[string]interface{}{
		"contentType": contentType[0],
	}
	var rawResp rawResponse
	if rawResp, err = ud.httpPOST(data, "/session", ud.sessionId, "appium/device/get_clipboard"); err != nil {
		return
	}
	reply := new(struct{ Value string })
	if err = json.Unmarshal(rawResp, reply); err != nil {
		return
	}

	if data, err := base64.StdEncoding.DecodeString(reply.Value); err != nil {
		raw.Write([]byte(reply.Value))
	} else {
		raw.Write(data)
	}
	return
}

func (ud *uiaDriver) SendKeys(text string, options ...DataOption) (err error) {
	// register(postHandler, new SendKeysToElement("/wd/hub/session/:sessionId/keys"))
	// https://github.com/appium/appium-uiautomator2-server/blob/master/app/src/main/java/io/appium/uiautomator2/handler/SendKeysToElement.java#L76-L85
	data := map[string]interface{}{
		"text": text,
	}
	// append options in post data for extra WDA configurations
	// e.g. use WithPressDuration to set pressForDuration
	for _, option := range options {
		option(data)
	}

	if _, ok := data["isReplace"]; !ok {
		data["isReplace"] = true // default true
	}

	_, err = ud.httpPOST(data, "/session", ud.sessionId, "keys")
	return
}

func (ud *uiaDriver) KeyboardDismiss(keyNames ...string) (err error) {
	// TODO
	return errDriverNotImplemented
}

func (ud *uiaDriver) PressButton(devBtn DeviceButton) (err error) {
	// TODO
	return errDriverNotImplemented
}

func (ud *uiaDriver) IOHIDEvent(pageID EventPageID, usageID EventUsageID, duration ...float64) (err error) {
	// TODO
	return errDriverNotImplemented
}

func (ud *uiaDriver) ExpectNotification(notifyName string, notifyType NotificationType, second ...int) (err error) {
	// register(postHandler, new OpenNotification("/wd/hub/session/:sessionId/appium/device/open_notifications"))
	_, err = ud.httpPOST(nil, "/session", ud.sessionId, "appium/device/open_notifications")
	return
}

func (ud *uiaDriver) SiriActivate(text string) (err error) {
	// TODO
	return errDriverNotImplemented
}

func (ud *uiaDriver) SiriOpenUrl(url string) (err error) {
	// TODO
	return errDriverNotImplemented
}

func (ud *uiaDriver) Orientation() (orientation Orientation, err error) {
	// register(getHandler, new GetOrientation("/wd/hub/session/:sessionId/orientation"))
	var rawResp rawResponse
	if rawResp, err = ud.httpGET("/session", ud.sessionId, "orientation"); err != nil {
		return "", err
	}
	reply := new(struct{ Value Orientation })
	if err = json.Unmarshal(rawResp, reply); err != nil {
		return "", err
	}

	orientation = reply.Value
	return
}

func (ud *uiaDriver) SetOrientation(orientation Orientation) (err error) {
	// TODO
	return errDriverNotImplemented
}

func (ud *uiaDriver) Rotation() (rotation Rotation, err error) {
	// register(getHandler, new GetRotation("/wd/hub/session/:sessionId/rotation"))
	var rawResp rawResponse
	if rawResp, err = ud.httpGET("/session", ud.sessionId, "rotation"); err != nil {
		return Rotation{}, err
	}
	reply := new(struct{ Value Rotation })
	if err = json.Unmarshal(rawResp, reply); err != nil {
		return Rotation{}, err
	}

	rotation = reply.Value
	return
}

func (ud *uiaDriver) SetRotation(rotation Rotation) (err error) {
	// TODO
	return errDriverNotImplemented
}

func (ud *uiaDriver) MatchTouchID(isMatch bool) (err error) {
	// TODO
	return errDriverNotImplemented
}

func (ud *uiaDriver) _findElements(method, selector string, elementID ...string) (elements []WebElement, err error) {
	// register(postHandler, new FindElements("/wd/hub/session/:sessionId/elements"))
	data := map[string]interface{}{
		"strategy": method,
		"selector": selector,
	}
	if len(elementID) != 0 {
		data["context"] = elementID[0]
	}
	var rawResp rawResponse
	if rawResp, err = ud.httpPOST(data, "/session", ud.sessionId, "/elements"); err != nil {
		return nil, err
	}
	reply := new(struct{ Value []map[string]string })
	if err = json.Unmarshal(rawResp, reply); err != nil {
		return nil, err
	}
	if len(reply.Value) == 0 {
		return nil, fmt.Errorf("no such element: unable to find an element using '%s', value '%s'", method, selector)
	}
	elements = make([]WebElement, len(reply.Value))
	for i, elem := range reply.Value {
		var id string
		if id = elementIDFromValue(elem); id == "" {
			return nil, fmt.Errorf("invalid element returned: %+v", reply)
		}
		uie := WebElement(uiaElement{parent: ud, id: id})
		elements[i] = uie
	}
	return
}

func (ud *uiaDriver) _findElement(method, selector string, elementID ...string) (elem *uiaElement, err error) {
	// register(postHandler, new FindElement("/wd/hub/session/:sessionId/element"))
	data := map[string]interface{}{
		"strategy": method,
		"selector": selector,
	}
	if len(elementID) != 0 {
		data["context"] = elementID[0]
	}
	var rawResp rawResponse
	if rawResp, err = ud.httpPOST(data, "/session", ud.sessionId, "/element"); err != nil {
		return nil, err
	}
	reply := new(struct{ Value map[string]string })
	if err = json.Unmarshal(rawResp, reply); err != nil {
		return nil, err
	}
	if len(reply.Value) == 0 {
		return nil, fmt.Errorf("no such element: unable to find an element using '%s', value '%s'", method, selector)
	}
	var id string
	if id = elementIDFromValue(reply.Value); id == "" {
		return nil, fmt.Errorf("invalid element returned: %+v", reply)
	}
	elem = &uiaElement{parent: ud, id: id}
	return
}

func (ud *uiaDriver) ActiveElement() (element WebElement, err error) {
	// TODO
	return element, errDriverNotImplemented
}

func (ud *uiaDriver) FindElement(by BySelector) (element WebElement, err error) {
	return ud._findElement(by.getUsingAndValue())
}

func (ud *uiaDriver) FindElements(by BySelector) (elements []WebElement, err error) {
	// [[FBRoute POST:@"/elements"] respondWithTarget:self action:@selector(handleFindElements:)]
	using, value := by.getUsingAndValue()
	data := map[string]interface{}{
		"using": using,
		"value": value,
	}
	var rawResp rawResponse
	if rawResp, err = ud.httpPOST(data, "/session", ud.sessionId, "/elements"); err != nil {
		return nil, err
	}
	var elementIDs []string
	if elementIDs, err = rawResp.valueConvertToElementIDs(); err != nil {
		if errors.Is(err, errNoSuchElement) {
			return nil, fmt.Errorf("%w: unable to find an element using '%s', value '%s'", err, using, value)
		}
		return nil, err
	}
	elements = make([]WebElement, len(elementIDs))
	for i := range elementIDs {
		elements[i] = WebElement(uiaElement{parent: ud, id: elementIDs[i]})
	}
	return
}

func (ud *uiaDriver) Screenshot() (raw *bytes.Buffer, err error) {
	// register(getHandler, new CaptureScreenshot("/wd/hub/session/:sessionId/screenshot"))
	var rawResp rawResponse
	if rawResp, err = ud.httpGET("/session", ud.sessionId, "screenshot"); err != nil {
		return nil, err
	}
	reply := new(struct{ Value string })
	if err = json.Unmarshal(rawResp, reply); err != nil {
		return nil, err
	}

	var decodeStr []byte
	if decodeStr, err = base64.StdEncoding.DecodeString(reply.Value); err != nil {
		return nil, err
	}

	raw = bytes.NewBuffer(decodeStr)
	return
}

func (ud *uiaDriver) Source(srcOpt ...SourceOption) (source string, err error) {
	// register(getHandler, new Source("/wd/hub/session/:sessionId/source"))
	var rawResp rawResponse
	if rawResp, err = ud.httpGET("/session", ud.sessionId, "source"); err != nil {
		return "", err
	}
	reply := new(struct{ Value string })
	if err = json.Unmarshal(rawResp, reply); err != nil {
		return "", err
	}

	source = reply.Value
	return
}

func (ud *uiaDriver) AccessibleSource() (source string, err error) {
	// TODO
	return source, errDriverNotImplemented
}

func (ud *uiaDriver) HealthCheck() (err error) {
	// TODO
	return errDriverNotImplemented
}

func (ud *uiaDriver) GetAppiumSettings() (settings map[string]interface{}, err error) {
	// register(getHandler, new GetSettings("/wd/hub/session/:sessionId/appium/settings"))
	var rawResp rawResponse
	if rawResp, err = ud.httpGET("/session", ud.sessionId, "appium/settings"); err != nil {
		return nil, err
	}
	reply := new(struct{ Value map[string]interface{} })
	if err = json.Unmarshal(rawResp, reply); err != nil {
		return nil, err
	}

	settings = reply.Value
	return
}

func (ud *uiaDriver) SetAppiumSettings(settings map[string]interface{}) (ret map[string]interface{}, err error) {
	data := map[string]interface{}{
		"settings": settings,
	}
	// register(postHandler, new UpdateSettings("/wd/hub/session/:sessionId/appium/settings"))
	_, err = ud.httpPOST(data, "/session", ud.sessionId, "appium/settings")
	return
}

func (ud *uiaDriver) IsHealthy() (healthy bool, err error) {
	// TODO
	return healthy, errDriverNotImplemented
}

func (ud *uiaDriver) WaitWithTimeoutAndInterval(condition Condition, timeout, interval time.Duration) error {
	startTime := time.Now()
	for {
		done, err := condition(ud)
		if err != nil {
			return err
		}
		if done {
			return nil
		}

		if elapsed := time.Since(startTime); elapsed > timeout {
			return fmt.Errorf("timeout after %v", elapsed)
		}
		time.Sleep(interval)
	}
}

func (ud *uiaDriver) WaitWithTimeout(condition Condition, timeout time.Duration) error {
	return ud.WaitWithTimeoutAndInterval(condition, timeout, DefaultWaitInterval)
}

func (ud *uiaDriver) Wait(condition Condition) error {
	return ud.WaitWithTimeoutAndInterval(condition, DefaultWaitTimeout, DefaultWaitInterval)
}
