/*
 * Copyright 2011-2023 the original author or authors.
 *
 * 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
 *
 *      https://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 org.springframework.data.redis.connection.jedis;

import redis.clients.jedis.BinaryJedisPubSub;
import redis.clients.jedis.ListPosition;
import redis.clients.jedis.Protocol;
import redis.clients.jedis.SortingParams;
import redis.clients.jedis.exceptions.JedisConnectionException;
import redis.clients.jedis.exceptions.JedisDataException;
import redis.clients.jedis.exceptions.JedisException;
import redis.clients.jedis.util.SafeEncoder;

import java.io.IOException;
import java.io.StringReader;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.TimeoutException;

import org.springframework.dao.DataAccessException;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.redis.RedisConnectionFailureException;
import org.springframework.data.redis.RedisSystemException;
import org.springframework.data.redis.connection.DefaultTuple;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.data.redis.connection.RedisListCommands.Position;
import org.springframework.data.redis.connection.RedisZSetCommands.Tuple;
import org.springframework.data.redis.connection.ReturnType;
import org.springframework.data.redis.connection.SortParameters;
import org.springframework.data.redis.connection.SortParameters.Order;
import org.springframework.data.redis.connection.SortParameters.Range;
import org.springframework.util.Assert;

/**
 * Helper class featuring methods for Jedis connection handling, providing support for exception translation. Deprecated
 * in favor of {@link JedisConverters}
 *
 * @author Costin Leau
 * @author Jennifer Hickey
 */
@Deprecated
public abstract class JedisUtils {

	private static final String OK_CODE = "OK";
	private static final String OK_MULTI_CODE = "+OK";
	private static final byte[] ONE = new byte[] { '1' };
	private static final byte[] ZERO = new byte[] { '0' };

	/**
	 * Converts the given, native Jedis exception to Spring's DAO hierarchy.
	 *
	 * @param ex Jedis exception
	 * @return converted exception
	 */
	public static DataAccessException convertJedisAccessException(JedisException ex) {
		if (ex instanceof JedisDataException) {
			return new InvalidDataAccessApiUsageException(ex.getMessage(), ex);
		}
		if (ex instanceof JedisConnectionException) {
			return new RedisConnectionFailureException(ex.getMessage(), ex);
		}

		// fallback to invalid data exception
		return new InvalidDataAccessApiUsageException(ex.getMessage(), ex);
	}

	/**
	 * Converts the given, native, runtime Jedis exception to Spring's DAO hierarchy.
	 *
	 * @param ex Jedis runtime/unchecked exception
	 * @return converted exception
	 */
	public static DataAccessException convertJedisAccessException(RuntimeException ex) {
		if (ex instanceof JedisException) {
			return convertJedisAccessException((JedisException) ex);
		}

		return null;
	}

	static DataAccessException convertJedisAccessException(IOException ex) {
		if (ex instanceof UnknownHostException) {
			return new RedisConnectionFailureException("Unknown host " + ex.getMessage(), ex);
		}
		return new RedisConnectionFailureException("Could not connect to Redis server", ex);
	}

	static DataAccessException convertJedisAccessException(TimeoutException ex) {
		throw new RedisConnectionFailureException("Jedis pool timed out. Could not get Redis Connection", ex);
	}

	static boolean isStatusOk(String status) {
		return status != null && (OK_CODE.equals(status) || OK_MULTI_CODE.equals(status));
	}

	/**
	 * Deprecated. Use #{@link JedisConverters#toBoolean(Long)}
	 */
	@Deprecated
	static Boolean convertCodeReply(Number code) {
		return (code != null ? code.intValue() == 1 : null);
	}

	/**
	 * Deprecated. Use #{@link JedisConverters#toTupleSet(Set)}
	 */
	@Deprecated
	static Set<Tuple> convertJedisTuple(Set<redis.clients.jedis.Tuple> tuples) {
		Set<Tuple> value = new LinkedHashSet<>(tuples.size());
		for (redis.clients.jedis.Tuple tuple : tuples) {
			value.add(new DefaultTuple(tuple.getBinaryElement(), tuple.getScore()));
		}

		return value;
	}

	static byte[][] convert(Map<byte[], byte[]> hgetAll) {
		byte[][] result = new byte[hgetAll.size() * 2][];

		int index = 0;
		for (Map.Entry<byte[], byte[]> entry : hgetAll.entrySet()) {
			result[index++] = entry.getKey();
			result[index++] = entry.getValue();
		}
		return result;
	}

	/**
	 * Deprecated. Implement a method in {@link JedisConverters} instead
	 */
	@Deprecated
	static Map<String, String> convert(String[] fields, String[] values) {
		Map<String, String> result = new LinkedHashMap<>(fields.length);

		for (int i = 0; i < values.length; i++) {
			result.put(fields[i], values[i]);
		}
		return result;
	}

	/**
	 * Deprecated. Implement a method in {@link JedisConverters} instead
	 */
	@Deprecated
	static String[] arrange(String[] keys, String[] values) {
		String[] result = new String[keys.length * 2];

		for (int i = 0; i < keys.length; i++) {
			int index = i << 1;
			result[index] = keys[i];
			result[index + 1] = values[i];

		}
		return result;
	}

	static SortingParams convertSortParams(SortParameters params) {
		SortingParams jedisParams = null;

		if (params != null) {
			jedisParams = new SortingParams();

			byte[] byPattern = params.getByPattern();
			if (byPattern != null) {
				jedisParams.by(params.getByPattern());
			}

			byte[][] getPattern = params.getGetPattern();
			if (getPattern != null) {
				jedisParams.get(getPattern);
			}

			Range limit = params.getLimit();
			if (limit != null) {
				jedisParams.limit((int) limit.getStart(), (int) limit.getCount());
			}
			Order order = params.getOrder();
			if (order != null && order.equals(Order.DESC)) {
				jedisParams.desc();
			}
			Boolean isAlpha = params.isAlphabetic();
			if (isAlpha != null && isAlpha) {
				jedisParams.alpha();
			}
		}

		return jedisParams;
	}

	/**
	 * Deprecated. Use #{@link JedisConverters#toBit(Boolean)}
	 */
	@Deprecated
	static byte[] asBit(boolean value) {
		return (value ? ONE : ZERO);
	}

	static ListPosition convertPosition(Position where) {
		Assert.notNull(where, "list positions are mandatory");
		return (Position.AFTER.equals(where) ? ListPosition.AFTER : ListPosition.BEFORE);
	}

	/**
	 * Deprecated. Use #{@link JedisConverters#toProperties(String)}
	 */
	@Deprecated
	static Properties info(String string) {
		Properties info = new Properties();
		StringReader stringReader = new StringReader(string);
		try {
			info.load(stringReader);
		} catch (Exception ex) {
			throw new RedisSystemException("Cannot read Redis info", ex);
		} finally {
			stringReader.close();
		}
		return info;
	}

	static BinaryJedisPubSub adaptPubSub(MessageListener listener) {
		return new JedisMessageListener(listener);
	}

	/**
	 * Deprecated. Use #{@link JedisConverters#toStrings(byte[][])}
	 */
	@Deprecated
	static String[] convert(byte[]... raw) {
		String[] result = new String[raw.length];

		for (int i = 0; i < raw.length; i++) {
			result[i] = SafeEncoder.encode(raw[i]);
		}

		return result;
	}

	static byte[][] bXPopArgs(int timeout, byte[]... keys) {
		List<byte[]> args = new ArrayList<>();
		for (byte[] arg : keys) {
			args.add(arg);
		}
		args.add(Protocol.toByteArray(timeout));
		return args.toArray(new byte[args.size()][]);
	}

	/**
	 * Deprecated. Use #{@link JedisConverters#toBytes(Integer)}
	 */
	@Deprecated
	static byte[] asBytes(int number) {
		return String.valueOf(number).getBytes();
	}

	/**
	 * Deprecated. Use #{@link JedisConverters#toString(byte[])}
	 */
	@Deprecated
	static String asString(byte[] raw) {
		return SafeEncoder.encode(raw);
	}

	@SuppressWarnings("unchecked")
	static Object convertScriptReturn(ReturnType returnType, Object result) {
		if (result instanceof String) {
			// evalsha converts byte[] to String. Convert back for consistency
			return SafeEncoder.encode((String) result);
		}
		if (returnType == ReturnType.STATUS) {
			return JedisUtils.asString((byte[]) result);
		}
		if (returnType == ReturnType.BOOLEAN) {
			// Lua false comes back as a null bulk reply
			if (result == null) {
				return Boolean.FALSE;
			}
			return ((Long) result == 1);
		}
		if (returnType == ReturnType.MULTI) {
			List<Object> resultList = (List<Object>) result;
			List<Object> convertedResults = new ArrayList<>();
			for (Object res : resultList) {
				if (res instanceof String) {
					// evalsha converts byte[] to String. Convert back for consistency
					convertedResults.add(SafeEncoder.encode((String) res));
				} else {
					convertedResults.add(res);
				}
			}
			return convertedResults;
		}
		return result;
	}

}
