import 'dart:convert';

import '../../common/exception.dart';
import '../../common/request.dart';
import '../../common/web_element.dart';

/// Magic constants -- identifiers indicating a value is an element.
/// Source: https://github.com/SeleniumHQ/selenium/wiki/JsonWireProtocol
const String jsonWireElementStr = 'ELEMENT';

Object? parseJsonWireResponse(WebDriverResponse response,
    {bool valueOnly = true}) {
  final statusCode = response.statusCode!;
  Map<String, dynamic> responseBody;
  try {
    responseBody = json.decode(response.body!) as Map<String, dynamic>;
  } catch (e) {
    final rawBody = response.body == null || response.body!.isEmpty
        ? '<empty response>'
        : response.body;
    if (statusCode == 405) {
      throw UnknownCommandException(statusCode, rawBody);
    }
    throw WebDriverException(
        statusCode, 'Error parsing response body: $rawBody');
  }

  if (statusCode < 200 ||
      statusCode > 299 ||
      (responseBody['status'] != null && responseBody['status'] != 0)) {
    final status = responseBody['status'] as int?;
    final message = responseBody['value']['message'] as String?;

    switch (status) {
      case 0:
        throw StateError('Not a WebDriverError Status: 0 Message: $message');
      case 6:
        throw NoSuchDriverException(status, message);
      case 7:
        throw NoSuchElementException(status, message);
      case 8:
        throw NoSuchFrameException(status, message);
      case 9:
        throw UnknownCommandException(status, message);
      case 10:
        throw StaleElementReferenceException(status, message);
      case 11:
        throw ElementNotVisibleException(status, message);
      case 12:
        throw InvalidElementStateException(status, message);
      case 15:
        throw ElementIsNotSelectableException(status, message);
      case 17:
        throw JavaScriptException(status, message);
      case 19:
        throw XPathLookupException(status, message);
      case 21:
        throw TimeoutException(status, message);
      case 23:
        throw NoSuchWindowException(status, message);
      case 24:
        throw InvalidCookieDomainException(status, message);
      case 25:
        throw UnableToSetCookieException(status, message);
      case 26:
        throw UnexpectedAlertOpenException(status, message);
      case 27:
        throw NoSuchAlertException(status, message);
      case 28:
        throw ScriptTimeoutException(status, message);
      case 29:
        throw InvalidElementCoordinatesException(status, message);
      case 30:
        throw IMENotAvailableException(status, message);
      case 31:
        throw IMEEngineActivationFailedException(status, message);
      case 32:
        throw InvalidSelectorException(status, message);
      case 33:
        throw SessionNotCreatedException(status, message);
      case 34:
        throw MoveTargetOutOfBoundsException(status, message);
      case 13:
        throw UnknownException(status, message);
      default:
        throw WebDriverException(status, message);
    }
  }

  if (valueOnly) {
    return responseBody['value'];
  }

  return responseBody;
}

/// Prefix to represent element in webdriver uri.
///
/// When [elementId] is null, it means root element.
String elementPrefix(String? elementId) =>
    elementId == null ? '' : 'element/$elementId/';

/// Deserializes json object returned by WebDriver server.
///
/// Mainly it handles the element object rebuild.
Object? deserialize(Object? result, dynamic Function(String) createElement) {
  if (result is Map) {
    if (result.containsKey(jsonWireElementStr)) {
      return createElement(result[jsonWireElementStr] as String);
    } else {
      final newResult = {};
      result.forEach((key, value) {
        newResult[key] = deserialize(value, createElement);
      });
      return newResult;
    }
  } else if (result is List) {
    return result.map((item) => deserialize(item, createElement)).toList();
  } else {
    return result;
  }
}

Object? serialize(dynamic obj) {
  if (obj is WebElement) {
    return {jsonWireElementStr: obj.id};
  }

  if (obj is Map) {
    return <String, dynamic>{
      for (var item in obj.entries) item.key as String: serialize(item.value),
    };
  }

  if (obj is List) {
    return [for (var item in obj) serialize(item)];
  }

  return obj;
}
