// Copyright 2016 Xiaomi, Inc.
//
// 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.xiaomi.filecache.ec.redis;

import com.xiaomi.filecache.ec.ECodec;
import com.xiaomi.filecache.ec.exceptions.ECFileCacheException;
import com.xiaomi.filecache.ec.redis.commands.RedisGetInfo;
import com.xiaomi.filecache.ec.redis.commands.RedisPutInfo;
import com.xiaomi.filecache.ec.utils.Pair;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.Validate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

public abstract class RedisAccessBase {
  public static final String SEP = "_";
  private static final String INFO = "info";

  public static final String GET_JEDIS_POOLS = "getJedisPools";
  public static final String GET_CHUNK_POS_AND_SIZE = " getChunkPosAndSize";
  public static final String GET_CHUNK = "getChunk";
  public static final String GET = "get";
  public static final String PUT = "put";
  public static final String CHECK_REDIS_RESULT = "checkRedisResult";

  private Map<Integer, DecoratedJedisPool> keyedPool = new HashMap<Integer, DecoratedJedisPool>();

  static final Logger LOGGER = LoggerFactory.getLogger(RedisAccessBase.class.getName());

  /**
   * Constructs Redis access. Create Redis pool for all Redis
   *
   * @param redisMap Redis index and address
   */
  public RedisAccessBase(Map<Integer, String> redisMap) {
    try {
      keyedPool.clear();
      for (Map.Entry<Integer, String> entry : redisMap.entrySet()) {
        int redisId = entry.getKey();

        String redisAddress = entry.getValue();
        DecoratedJedisPool decoratedJedisPool = DecoratedJedisPool.create(redisAddress);

        keyedPool.put(redisId, decoratedJedisPool);

        LOGGER.info("add redis[{}] to keyedPool, key[{}]", redisAddress, redisId);
      }
    } catch (ECFileCacheException e) {
      keyedPool.clear();
    }
  }

  /**
   * Put chunk data of file to cache
   *
   * @param redisIds Redis indexes indicate which Redis to use
   * @param cacheKey key of cached file
   * @param chunkPos chunk offset in file, used as chunk key
   * @param chunks chunk of file to cache
   * @throws ECFileCacheException
   */
  public abstract void put(List<Integer> redisIds, String cacheKey, long chunkPos, final byte[][] chunks) throws ECFileCacheException;

  /**
   * Get file data from cache
   *
   * @param redisIds Redis indexes indicate which Redis to use
   * @param cacheKey key of cached file
   * @return all chunks of file. need to reorder and decode to get the origin file
   * @throws ECFileCacheException
   */
  public abstract List<Pair<byte[][], int[]>> get(List<Integer> redisIds, String cacheKey) throws ECFileCacheException;

  /**
   * Get a specified chunk from cache
   *
   * @param redisIds Redis indexes indicate which Redis to use
   * @param cacheKey key of cached file
   * @param chunkPos chunk offset in file, used as chunk key
   * @param chunkSize length of chunk data
   * @return
   * @throws ECFileCacheException
   */
  public abstract Pair<byte[][], int[]> getChunk(List<Integer> redisIds, String cacheKey, long chunkPos, int chunkSize)
      throws ECFileCacheException;

  /**
   * Get all chunks' info, named offset and size of chunk
   *
   * @param redisIds Redis indexes indicate which Redis to use
   * @param cacheKey key of cached file
   * @return all chunks' offset and size. The key of returned map is chunk offset, value is chunk size
   * @throws ECFileCacheException
   */
  public abstract Map<Long, Integer> getChunkPosAndSize(List<Integer> redisIds, String cacheKey) throws ECFileCacheException;

  /**
   * Delete cached data of key
   *
   * @param redisIds Redis indexes indicate which Redis to use
   * @param cacheKey key of cached file
   * @throws ECFileCacheException
   */
  public abstract void delete(List<Integer> redisIds, String cacheKey) throws ECFileCacheException;

  /**
   * Put info data to a cache node
   * Use only one Redis, info may be lost when redis restart
   * So do not save important data by this method
   *
   * @param redisId Redis index indicate which Redis to use
   * @param cacheKey key of cache info
   * @param data cache info
   */
  public void putInfo(int redisId, String cacheKey, final byte[] data) {
    DecoratedJedisPool jedisPool = keyedPool.get(redisId);
    if (jedisPool == null) {
      String verbose = String.format("put info: have no jedis pool of id[%d]", redisId);
      LOGGER.warn(verbose);
      return;
    }

    cacheKey = cacheKey + SEP + INFO;
    RedisPutInfo redisPutInfo = new RedisPutInfo(jedisPool, cacheKey, data);

    try {
      redisPutInfo.call();
    } catch (Exception ignore) {
      // ignore exception
    }
  }

  /**
   * Get info from cache node
   *
   * @param redisId Redis index indicate which Redis to use
   * @param cacheKey key of cache info
   * @return cached info data, maybe null
   */
  /*@ Nullable */
  public byte[] getInfo(int redisId, String cacheKey) {
    DecoratedJedisPool jedisPool = keyedPool.get(redisId);
    if (jedisPool == null) {
      String verbose = String.format("get info: have no jedis pool of id[%d]", redisId);
      LOGGER.warn(verbose);
      return null;
    }

    cacheKey = cacheKey + SEP + INFO;
    List<byte[]> result = new ArrayList<byte[]>(1);
    RedisGetInfo redisGetInfo = new RedisGetInfo(jedisPool, cacheKey, result);
    try {
      if (0 == redisGetInfo.call()) {
        return result.get(0);
      }
    } catch (Exception ignore) {
      // ignore exception
    }
    return null;
  }

  protected List<DecoratedJedisPool> getJedisPools(List<Integer> redisIds) throws ECFileCacheException {
    List<DecoratedJedisPool> jedisPools = new ArrayList<DecoratedJedisPool>();
    int failCount = 0;
    for (Integer redisId : redisIds) {

      DecoratedJedisPool jedisPool = keyedPool.get(redisId);
      jedisPools.add(jedisPool);

      if (jedisPool == null) {
        String verbose = String.format("have no jedis pool of id[%d]", redisId);
        LOGGER.warn(verbose);
        failCount++;
      }
      checkFail(failCount, GET_JEDIS_POOLS, "NIL");
    }
    return jedisPools;
  }


  public Map<Integer, DecoratedJedisPool> getKeyedPool() {
    return keyedPool;
  }

  /**
   * Check whether times of access Redis failed is bigger than ECodec.CODING_BLOCK_NUM
   *
   * @param failCount times of access Redis failed
   * @param method method name for log
   * @param key the key of cached data
   * @throws ECFileCacheException
   */
  static void checkFail(int failCount, String method, String key) throws ECFileCacheException {
    if (failCount > ECodec.CODING_BLOCK_NUM) {
      String verbose = String.format("[%s]:get cached data key[%s] fail count > CODING_BLOCK_NUM. [%d] > [%d]",
          method, key, failCount, ECodec.CODING_BLOCK_NUM);
      LOGGER.error(verbose);
      throw new ECFileCacheException(verbose);
    }
  }

  /**
   * Check every row's length of 2D array is same
   *
   * @param data 2D array
   * @return row's length of 2D array
   * @throws ECFileCacheException
   */
  static int checkDataAndGetLength(byte[][] data) throws ECFileCacheException {
    int length = -1;
    for (final byte[] aData : data) {
      if (ArrayUtils.isEmpty(aData)) {
        String verbose = "invalid put data. empty data";
        LOGGER.error(verbose);
        throw new ECFileCacheException(verbose);
      }

      if (length == -1) {
        length = aData.length;
      } else if (aData.length != length) {
        String verbose = "invalid put data. data length is not equal";
        LOGGER.error(verbose);
        throw new ECFileCacheException(verbose);
      }
    }
    return length;
  }

  /**
   * Convert data read from redis to list
   *
   * @param redisDataList  array of Redis hash map that read from Redis nodes
   * @return list with pair element of chunk data and erasures info
   * @throws ECFileCacheException
   */
  static List<Pair<byte[][], int[]>> convert(Map<byte[], byte[]>[] redisDataList) throws ECFileCacheException {
    int redisDataNum = redisDataList.length;

    @SuppressWarnings("unchecked")
    Set<byte[]>[] redisFields = new Set[redisDataNum];
    List<Map<String, byte[]>> redisDataListTemp = new ArrayList<Map<String, byte[]>>();

    // pre-process redis data
    // get hashmap from every redis
    // get all hashmap key, named all chunkId and chunkSize
    // convert to Long and sort
    for (int i = 0; i < redisDataNum; ++i) {
      Set<byte[]> fieldSet = new HashSet<byte[]>();
      Map<String, byte[]> redisDataMap = new HashMap<String, byte[]>();

      Map<byte[], byte[]> map = redisDataList[i];
      if (MapUtils.isNotEmpty(map)) {
        for (Map.Entry<byte[], byte[]> entry : map.entrySet()) {
          fieldSet.add(entry.getKey());

          String chunkPosAndSizeStr = new String(entry.getKey());
          redisDataMap.put(chunkPosAndSizeStr, entry.getValue());
        }
      }
      redisFields[i] = fieldSet;
      redisDataListTemp.add(redisDataMap);
    }

    Map<Long, Integer> chunkPosAndSize = convertChunkPosAndSize(redisFields);

    // process redis data by chunkId
    // generate a zero-filled array if chunk data is empty or chunk size is not match
    List<Pair<byte[][], int[]>> chunkAndErasuresList = new ArrayList<Pair<byte[][], int[]>>();
    for (Map.Entry<Long, Integer> entry : chunkPosAndSize.entrySet()){
      Long chunkId = entry.getKey();
      int chunkSize = entry.getValue();
      String chunkPosAndSizeStr = chunkId + SEP + chunkSize;

      byte[][] chunks = new byte[redisDataNum][];

      int i = 0;
      for (Map<String, byte[]> map : redisDataListTemp) {
        chunks[i++] = map.get(chunkPosAndSizeStr);
      }
      Pair<byte[][], int[]> pair = convertChunk(chunks, chunkSize);
      chunkAndErasuresList.add(pair);
    }

    return chunkAndErasuresList;
  }

  /**
   * Convert chunks get from Redis.
   * Make a zero-filled array if failed to read chunk from Redis.
   *
   * @param redisDataArray chunks get from all Redis nodes
   * @param chunkSize length of chunk data
   * @return pair of chunks and erasures array
   */
  static Pair<byte[][], int[]> convertChunk(byte[][] redisDataArray, int chunkSize) {
    Validate.isTrue(ArrayUtils.isNotEmpty(redisDataArray));

    List<Integer> erasures = new ArrayList<Integer>();

    int i = 0;
    for (byte[] chunk : redisDataArray) {
      // can not read chunk data from redis
      // make a zero-filled array for ec decode
      if (ArrayUtils.isEmpty(chunk) || chunk.length != chunkSize) {
        chunk = new byte[chunkSize];
        erasures.add(i);
      }
      redisDataArray[i++] = chunk;
    }
    return Pair.create(redisDataArray, adjustErasures(erasures, redisDataArray.length));
  }

  /**
   * Convert chunks info read from Redis to sorted map.
   *
   * @param redisFields chunks info get from Redis
   * @return map, with chunk offset as key and chunk size of value
   * @throws ECFileCacheException
   */
  static Map<Long, Integer> convertChunkPosAndSize(Set<byte[]>[] redisFields) throws ECFileCacheException {

    Map<String, Long> chunkPosSizeMap = getValidateChunks(redisFields);
    Set<Long> chunkPosSet = new HashSet<Long>(chunkPosSizeMap.values());

    Map<Long, Integer> chunkPosAndSize = new TreeMap<Long, Integer>();

    for (String chunkPosAndSizeStr : chunkPosSizeMap.keySet()) {

      String[] toks = chunkPosAndSizeStr.split(SEP);
      Validate.isTrue(toks.length == 2);

      long chunkPos = Long.parseLong(toks[0]);
      int chunkSize = Integer.parseInt(toks[1]);

      if (!chunkPosAndSize.containsKey(chunkPos) ||
          (chunkPosAndSize.get(chunkPos) > chunkSize && chunkPosSet.contains(chunkPos + chunkSize * ECodec.DATA_BLOCK_NUM))) {
        chunkPosAndSize.put(chunkPos, chunkSize);
      }
    }

    checkChunks(chunkPosAndSize);

    return chunkPosAndSize;
  }

  /**
   * Convert chunk info from Redis nodes to map.
   *
   * @param redisFields chunk offset and size info get from Redis. In format chunkPos_chunkSize.
   * @return map, with chunk info as map key and chunk offset as value
   */
  static Map<String, Long> getValidateChunks(Set<byte[]>[] redisFields) {
    Map<String, Integer> chunkPosAndSizeCount = new HashMap<String, Integer>();
    for (Set<byte[]> fields : redisFields) {
      if (CollectionUtils.isEmpty(fields)) {
        continue;
      }

      for (byte[] field : fields) {
        String chunkPosAndSizeStr = new String(field);
        if (chunkPosAndSizeCount.containsKey(chunkPosAndSizeStr)) {
          chunkPosAndSizeCount.put(chunkPosAndSizeStr, chunkPosAndSizeCount.get(chunkPosAndSizeStr) + 1);
        } else {
          chunkPosAndSizeCount.put(chunkPosAndSizeStr, 1);
        }
      }
    }

    Map<String, Long> chunkInfoAndPos = new HashMap<String, Long>();
    for (Map.Entry<String, Integer> entry : chunkPosAndSizeCount.entrySet()) {
      if (entry.getValue() >= ECodec.DATA_BLOCK_NUM) {

        String[] toks = entry.getKey().split(SEP);
        Validate.isTrue(toks.length == 2);

        long chunkPos = Long.parseLong(toks[0]);
        chunkInfoAndPos.put(entry.getKey(), chunkPos);
      }
    }

    return chunkInfoAndPos;
  }

  /**
   * Check chunks is continuous and have no overlap
   *
   * @param chunkPosAndSize chunks data
   * @throws ECFileCacheException
   */
  static void checkChunks(Map<Long, Integer> chunkPosAndSize) throws ECFileCacheException {
    long nextChunkPos = 0;
    for (Map.Entry<Long, Integer> entry : chunkPosAndSize.entrySet()) {
      long chunkPos = entry.getKey();
      int chunkSize = entry.getValue();

      if (nextChunkPos != chunkPos) {
        String verbose = String.format("lost chunk[%d], actual is [%d]", nextChunkPos, chunkPos);
        LOGGER.error(verbose);
        throw new ECFileCacheException(verbose);
      }

      nextChunkPos += chunkSize * ECodec.DATA_BLOCK_NUM;
    }
  }

  /**
   * Convert erasures info list to array
   *
   * @param erasures list with int element, save erased chunk index
   * @param ecBlockNum chunks' number
   * @return erasures array indicated which chunk was erased
   */
  static int[] adjustErasures(List<Integer> erasures, int ecBlockNum) {
    // erasures array should contain at least one element, required by libjerasure
    if (erasures.isEmpty()) {
      erasures.add(ecBlockNum - 1);
    }
    int[] erasuresInt = new int[erasures.size()];
    for (int i = 0, len = erasures.size(); i < len; ++i) {
      erasuresInt[i] = erasures.get(i);
    }
    return erasuresInt;
  }
}