/**
 * Copyright (c) 2025 Huawei Device Co., Ltd.
 * 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 ohos.ace.adapter;

import ohos.ace.adapter.ALog;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

/**
 * WantParams is a utility class for managing JSON parameters
 * that can be used across different platforms.
 *
 * @since 2025-01-15
 */
public class WantParams {
    private static final String LOG_TAG = "WantParams";
    private static final String WANT_PARAMS_PARAMS = "params";
    private static final String WANT_PARAMS_KEY = "key";
    private static final String WANT_PARAMS_TYPE = "type";
    private static final String WANT_PARAMS_VALUE = "value";
    private static final int VALUE_TYPE_BOOLEAN = 1;
    private static final int VALUE_TYPE_INT = 5;
    private static final int VALUE_TYPE_DOUBLE = 9;
    private static final int VALUE_TYPE_STRING = 10;
    private static final int VALUE_TYPE_WANT_PARAMS_ARRAY = 24;
    private static final int VALUE_TYPE_WANT_PARAMS = 101;
    private static final int VALUE_TYPE_ARRAY = 102;

    private final Map<String, WantValue> wantParamsMap;

    /**
     * Default constructor initializes the WantParams.
     */
    public WantParams() {
        wantParamsMap = new HashMap<>();
    }

    /**
     * Constructor initializes the WantParams from a JSON string.
     *
     * @param wantParamsStr the JSON string to initialize from
     * @throws JSONException if the JSON string is malformed
     */
    public WantParams(String wantParamsStr) {
        wantParamsMap = new HashMap<>();
        try {
            JSONObject wantObject = new JSONObject(wantParamsStr);
            parseWantParams(wantObject, WANT_PARAMS_PARAMS);
        } catch (JSONException jsonException) {
            ALog.e(LOG_TAG, "WantParams parse error.");
            jsonException.printStackTrace();
        }
    }

    /**
     * Inner class to hold the value and type of a parameter.
     */
    private static class WantValue {
        /**
         * The type of the value, represented as an integer constant.
         */
        public int type;

        /**
         * The actual value, which can be of any object type.
         */
        public Object value;

        /**
         * Constructs a new WantValue object with the specified type and value.
         *
         * @param type  the type of the value, represented as an integer constant
         * @param value the actual value, which can be of any object type
         */
        public WantValue(int type, Object value) {
            this.type = type;
            this.value = value;
        }
    }

    /**
     * Parses a JSONArray and populates the WantParams map.
     *
     * @param wantArray the JSONArray to parse
     * @return true if parsing was successful, false otherwise
     * @throws JSONException if the JSONArray is malformed
     */
    private boolean parse(JSONArray wantArray) throws JSONException {
        for (int i = 0; i < wantArray.length(); i++) {
            JSONObject jsonElement = wantArray.getJSONObject(i);
            if (!jsonElement.has(WANT_PARAMS_KEY) || !jsonElement.has(WANT_PARAMS_TYPE)
                    || !jsonElement.has(WANT_PARAMS_VALUE)) {
                ALog.e(LOG_TAG, "WantParams data format error.");
                return false;
            }
            int typeId = jsonElement.getInt(WANT_PARAMS_TYPE);
            String wantKey = jsonElement.getString(WANT_PARAMS_KEY);
            WantValue wantValue = null;
            switch (typeId) {
                case VALUE_TYPE_BOOLEAN:
                    wantValue = new WantValue(typeId, jsonElement.getBoolean(WANT_PARAMS_VALUE));
                    this.wantParamsMap.put(wantKey, wantValue);
                    break;
                case VALUE_TYPE_INT:
                    wantValue = new WantValue(typeId, jsonElement.getInt(WANT_PARAMS_VALUE));
                    this.wantParamsMap.put(wantKey, wantValue);
                    break;
                case VALUE_TYPE_DOUBLE:
                    wantValue = new WantValue(typeId, jsonElement.getDouble(WANT_PARAMS_VALUE));
                    this.wantParamsMap.put(wantKey, wantValue);
                    break;
                case VALUE_TYPE_STRING:
                    wantValue = new WantValue(typeId, jsonElement.getString(WANT_PARAMS_VALUE));
                    this.wantParamsMap.put(wantKey, wantValue);
                    break;
                case VALUE_TYPE_WANT_PARAMS:
                    WantParams localWantParams = new WantParams();
                    localWantParams.parse(jsonElement.getJSONArray(WANT_PARAMS_VALUE));
                    wantValue = new WantValue(typeId, localWantParams);
                    this.wantParamsMap.put(wantKey, wantValue);
                    break;
                case VALUE_TYPE_ARRAY:
                    parseWantArray(jsonElement);
                    break;
                default:
                    ALog.e(LOG_TAG, "Not supported data type");
                    break;
            }
        }
        return true;
    }

    /**
     * Parses a JSONObject representing an array and populates the WantParams map.
     *
     * @param jsonElement the JSONObject to parse
     * @throws JSONException if the JSONObject is malformed
     */
    private void parseWantArray(JSONObject jsonElement) throws JSONException {
        JSONArray localJsonArray = jsonElement.getJSONArray(WANT_PARAMS_VALUE);
        String wantKey = jsonElement.getString(WANT_PARAMS_KEY);
        WantValue wantValue = null;
        if (localJsonArray.length() < 1) {
            wantValue = new WantValue(VALUE_TYPE_ARRAY, new Object());
            this.wantParamsMap.put(wantKey, wantValue);
            return;
        }
        if (localJsonArray.get(0) instanceof Integer) {
            int[] convertArray = new int[localJsonArray.length()];
            for (int j = 0; j < localJsonArray.length(); j++) {
                convertArray[j] = localJsonArray.getInt(j);
            }
            wantValue = new WantValue(VALUE_TYPE_ARRAY, convertArray);
        } else if (localJsonArray.get(0) instanceof Boolean) {
            boolean[] convertArray = new boolean[localJsonArray.length()];
            for (int j = 0; j < localJsonArray.length(); j++) {
                convertArray[j] = localJsonArray.getBoolean(j);
            }
            wantValue = new WantValue(VALUE_TYPE_ARRAY, convertArray);
        } else if (localJsonArray.get(0) instanceof Double) {
            double[] convertArray = new double[localJsonArray.length()];
            for (int j = 0; j < localJsonArray.length(); j++) {
                convertArray[j] = localJsonArray.getDouble(j);
            }
            wantValue = new WantValue(VALUE_TYPE_ARRAY, convertArray);
        } else if (localJsonArray.get(0) instanceof String) {
            String[] convertArray = new String[localJsonArray.length()];
            for (int j = 0; j < localJsonArray.length(); j++) {
                convertArray[j] = localJsonArray.getString(j);
            }
            wantValue = new WantValue(VALUE_TYPE_ARRAY, convertArray);
        } else if (localJsonArray.get(0) instanceof JSONObject) {
            WantParams[] convertArray = new WantParams[localJsonArray.length()];
            WantParams wantParams;
            for (int j = 0; j < localJsonArray.length(); j++) {
                wantParams = new WantParams();
                wantParams.parseWantParams(localJsonArray.getJSONObject(j), WANT_PARAMS_VALUE);
                convertArray[j] = wantParams;
            }
            wantValue = new WantValue(VALUE_TYPE_WANT_PARAMS_ARRAY, convertArray);
        } else {
            ALog.e(LOG_TAG, "Not supported data type");
            return;
        }
        this.wantParamsMap.put(wantKey, wantValue);
    }

    /**
     * Parses a JSONObject and populates the WantParams map.
     *
     * @param wantObject the JSONObject to parse
     * @param wantKey    the key to look for in the JSONObject
     * @throws JSONException if the JSONObject is malformed
     */
    private void parseWantParams(JSONObject wantObject, String wantKey) throws JSONException {
        if (!wantObject.has(wantKey)) {
            return;
        }
        if (wantObject.get(wantKey) instanceof JSONArray) {
            JSONArray wantArray = (JSONArray) wantObject.get(wantKey);
            if (!parse(wantArray)) {
                ALog.e(LOG_TAG, "WantParams data format error.");
            }
        }
    }

    /**
     * Adds a boolean value to the WantParams.
     *
     * @param key   the key for the value
     * @param value the boolean value to add
     * @return this instance for method chaining
     */
    public WantParams addValue(String key, boolean value) {
        innerAddValue(key, value, VALUE_TYPE_BOOLEAN);
        return this;
    }

    /**
     * Adds a Object value to the WantParams.
     *
     * @param key   the key for the value
     * @param value the Object value to add
     * @param type  the type
     */
    private void innerAddValue(String key, Object value, int type) {
        if (key != null && !key.isEmpty()) {
            wantParamsMap.put(key, new WantValue(type, value));
        }
    }

    /**
     * Adds an int value to the WantParams.
     *
     * @param key   the key for the value
     * @param value the int value to add
     * @return this instance for method chaining
     */
    public WantParams addValue(String key, int value) {
        innerAddValue(key, value, VALUE_TYPE_INT);
        return this;
    }

    /**
     * Adds a float value to the WantParams.
     *
     * @param key   the key for the value
     * @param value the float value to add
     * @return this instance for method chaining
     */
    public WantParams addValue(String key, float value) {
        innerAddValue(key, value, VALUE_TYPE_DOUBLE);
        return this;
    }

    /**
     * Adds a double value to the WantParams.
     *
     * @param key   the key for the value
     * @param value the double value to add
     * @return this instance for method chaining
     */
    public WantParams addValue(String key, double value) {
        innerAddValue(key, value, VALUE_TYPE_DOUBLE);
        return this;
    }

    /**
     * Adds a String value to the WantParams.
     *
     * @param key   the key for the value
     * @param value the String value to add
     * @return this instance for method chaining
     */
    public WantParams addValue(String key, String value) {
        innerAddValue(key, value, VALUE_TYPE_STRING);
        return this;
    }

    /**
     * Adds an array of boolean values to the WantParams.
     *
     * @param key   the key for the value
     * @param value the array of boolean values to add
     * @return this instance for method chaining
     */
    public WantParams addValue(String key, boolean[] value) {
        innerAddArrayValue(key, value);
        return this;
    }

    /**
     * Adds an array of int values to the WantParams.
     *
     * @param key   the key for the value
     * @param value the array of int values to add
     * @return this instance for method chaining
     */
    public WantParams addValue(String key, int[] value) {
        innerAddArrayValue(key, value);
        return this;
    }

    /**
     * Adds an array of float values to the WantParams.
     *
     * @param key   the key for the value
     * @param value the array of float values to add
     * @return this instance for method chaining
     */
    public WantParams addValue(String key, float[] value) {
        innerAddArrayValue(key, value);
        return this;
    }

    /**
     * Adds an array of double values to the WantParams.
     *
     * @param key   the key for the value
     * @param value the array of double values to add
     * @return this instance for method chaining
     */
    public WantParams addValue(String key, double[] value) {
        innerAddArrayValue(key, value);
        return this;
    }

    /**
     * Adds an array of String values to the WantParams.
     *
     * @param key   the key for the value
     * @param value the array of String values to add
     * @return this instance for method chaining
     */
    public WantParams addValue(String key, String[] value) {
        innerAddArrayValue(key, value);
        return this;
    }

    /**
     * Adds an array of Object values to the WantParams.
     *
     * @param key   the key for the value
     * @param value the array of Object values to add
     */
    private void innerAddArrayValue(String key, Object value) {
        if (key != null && !key.isEmpty() && value != null) {
            this.wantParamsMap.put(key, new WantValue(VALUE_TYPE_ARRAY, value));
        }
    }

    /**
     * Adds a WantParams object to the WantParams.
     *
     * @param key   the key for the value
     * @param value the WantParams object to add
     * @return this instance for method chaining
     */
    public WantParams addValue(String key, WantParams value) {
        innerAddValue(key, value, VALUE_TYPE_WANT_PARAMS);
        return this;
    }

    /**
     * Retrieves a value from the WantParams.
     *
     * @param key the key for the value
     * @return the value associated with the key, or null if not found
     */
    public Object getValue(String key) {
        if (key != null && !key.isEmpty() && this.wantParamsMap.containsKey(key)) {
            return Objects.requireNonNull(this.wantParamsMap.get(key)).value;
        }
        return null;
    }

    private String replaceSpecialChars(String value) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < value.length(); i++) {
            char c = value.charAt(i);
            switch (c) {
                case '\\':
                    sb.append("\\\\");
                    break;
                case '\n':
                    sb.append("\\n");
                    break;
                case '\t':
                    sb.append("\\t");
                    break;
                case '\r':
                    sb.append("\\r");
                    break;
                case '\b':
                    sb.append("\\b");
                    break;
                case '\f':
                    sb.append("\\f");
                    break;
                case '\"':
                    sb.append("\\\"");
                    break;
                default:
                    sb.append(c);
                    break;
            }
        }
        return sb.toString();
    }

    /**
     * Converts the WantParams to a JSON string.
     *
     * @return the JSON string representation of the WantParams
     */
    private String innerToWantParamsString() {
        StringBuilder wantParamsString = new StringBuilder();
        wantParamsString.append("[");
        String valueStr;
        for (Map.Entry<String, WantValue> entry : wantParamsMap.entrySet()) {
            int typeId = entry.getValue().type;
            if (typeId == VALUE_TYPE_ARRAY) {
                try {
                    valueStr = new JSONArray(entry.getValue().value).toString();
                } catch (JSONException jsonException) {
                    ALog.e(LOG_TAG, "WantParams array toString failed.");
                    continue;
                }
            } else if (typeId == VALUE_TYPE_WANT_PARAMS) {
                if (entry.getValue().value instanceof WantParams) {
                    valueStr = ((WantParams) entry.getValue().value).innerToWantParamsString();
                } else {
                    continue;
                }
            } else if (typeId == VALUE_TYPE_WANT_PARAMS_ARRAY) {
                valueStr = wantArrayToString(entry);
            } else if (typeId == VALUE_TYPE_STRING) {
                valueStr = "\"" + replaceSpecialChars(entry.getValue().value.toString()) + "\"";
            } else {
                valueStr = entry.getValue().value.toString();
            }
            wantParamsString.append("{\"" + WANT_PARAMS_KEY + "\":\"");
            wantParamsString.append(entry.getKey());
            wantParamsString.append("\",\"" + WANT_PARAMS_TYPE + "\":");
            wantParamsString.append(
                    typeId == VALUE_TYPE_WANT_PARAMS_ARRAY ? VALUE_TYPE_ARRAY : typeId);
            wantParamsString.append(",\"" + WANT_PARAMS_VALUE + "\":");
            wantParamsString.append(valueStr);
            wantParamsString.append("},");
        }
        wantParamsString.deleteCharAt(wantParamsString.length() - 1);
        wantParamsString.append("]");
        return wantParamsString.toString();
    }

    /**
     * Converts an array of WantParams to a JSON string.
     *
     * @param entry the map entry containing the array
     * @return the JSON string representation of the array
     */
    private static String wantArrayToString(Map.Entry<String, WantValue> entry) {
        String valueStr;
        StringBuilder wantArrayString = new StringBuilder();
        wantArrayString.append("[");
        WantParams[] wantArray = ((WantParams[]) entry.getValue().value);
        for (WantParams wantElement : wantArray) {
            wantArrayString.append(
                    "{\"" + WANT_PARAMS_KEY + "\":\"" + WANT_PARAMS_PARAMS + "\",\"" + WANT_PARAMS_TYPE + "\":");
            wantArrayString.append(VALUE_TYPE_WANT_PARAMS);
            wantArrayString.append(",\"" + WANT_PARAMS_VALUE + "\":");
            wantArrayString.append(wantElement.innerToWantParamsString());
            wantArrayString.append("},");
        }
        wantArrayString.deleteCharAt(wantArrayString.length() - 1);
        wantArrayString.append("]");
        valueStr = wantArrayString.toString();
        return valueStr;
    }

    /**
     * Converts the WantParams to a JSON string with a version prefix.
     *
     * @return the JSON string representation of the WantParams with a version
     *         prefix
     */
    public String toWantParamsString() {
        return "{\"" + WANT_PARAMS_PARAMS + "\":" + this.innerToWantParamsString() + "}";
    }
}