/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 org.apache.ambari.metrics.core.timeline.uuid;

import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import org.apache.ambari.metrics.core.timeline.aggregators.TimelineClusterMetric;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang3.StringUtils;

public class HashBasedUuidGenStrategy extends MetricUuidGenNullRestrictedStrategy {

  /**
   * Computes the UUID for a timelineClusterMetric.
   * @param timelineClusterMetric
   * @param maxLength
   * @return byte array of length 'maxlength'
   */
  @Override
  protected byte[] computeUuidInternal(TimelineClusterMetric timelineClusterMetric, int maxLength) {

    int metricNameUuidLength = 12;
    String instanceId = timelineClusterMetric.getInstanceId();

    if ((StringUtils.isEmpty(instanceId))) {
      metricNameUuidLength = 14;
    }

    String metricName = timelineClusterMetric.getMetricName();

    //Compute the individual splits.
    String[] splits = getIndidivualSplits(metricName);

    /*
    Compute the weighted ascii sum of every split in the metric name. (asciiSum += (int) splits[s].charAt(i))
    These weighted sums are 'appended' to get the unique ID for metric name.
     */
    StringBuilder splitSums = new StringBuilder();
    long totalAsciiSum = 0l;
    if (splits.length > 0) {
      for (String split : splits) {
        int asciiSum = 0;
        for (int i = 0; i < split.length(); i++) {
          asciiSum += ((i + 1) * (int) split.charAt(i)); //weighted sum for split.
        }
        splitSums.append(asciiSum); //Append the sum to the array of sums.
        totalAsciiSum += asciiSum; //Parity Sum
      }
    }

    String splitSumString = totalAsciiSum + splitSums.reverse().toString(); //Reverse and attach parity sum.
    int splitLength = splitSumString.length();

    //Compute a unique metric seed for the stemmed metric name
    String stemmedMetric = stem(metricName);
    long metricSeed = 100123456789L;
    metricSeed += computeWeightedNumericalAsciiSum(stemmedMetric);
    //Reverse the computed seed to get a metric UUID portion which is used optionally.
    byte[] metricSeedBytes = StringUtils.reverse(String.valueOf(metricSeed)).getBytes();

    int seedLength = (int)(0.25 * metricNameUuidLength);
    int sumLength = metricNameUuidLength - seedLength;
    if (splitLength < sumLength) {
      sumLength = splitLength;
      seedLength = metricNameUuidLength - sumLength;
    }

    byte[] metricUuidPortion = ArrayUtils.addAll(
      ArrayUtils.subarray(splitSumString.getBytes(), 0, sumLength)
      , ArrayUtils.subarray(metricSeedBytes, 0, seedLength));

    /*
      For appId and instanceId the logic is similar. Use a seed integer to start with and compute ascii sum.
      Based on required length, use a suffix of the computed uuid.
     */
    String appId = timelineClusterMetric.getAppId();
    int appidSeed = 11;
    for (int i = 0; i < appId.length(); i++) {
      appidSeed += appId.charAt(i);
    }
    String appIdSeedStr = String.valueOf(appidSeed);
    byte[] appUuidPortion = ArrayUtils.subarray(appIdSeedStr.getBytes(), appIdSeedStr.length() - 2, appIdSeedStr.length());

    if (StringUtils.isNotEmpty(instanceId)) {
      byte[] instanceUuidPortion = new byte[2];
      ByteBuffer buffer = ByteBuffer.allocate(4);
      int instanceIdSeed = 1489;
      for (int i = 0; i < appId.length(); i++) {
        instanceIdSeed += ((i+1) * appId.charAt(i));
      }
      buffer.putInt(instanceIdSeed);
      ArrayUtils.subarray(buffer.array(), 2, 4);
      // Concatenate all UUIDs together (metric uuid + appId uuid + instanceId uuid)
      return ArrayUtils.addAll(ArrayUtils.addAll(metricUuidPortion, appUuidPortion), instanceUuidPortion);
    }

    return ArrayUtils.addAll(metricUuidPortion, appUuidPortion);
  }

  /**
   * Splits the metric name into individual tokens.
   * For example,
   *  kafka.server.ReplicaManager.LeaderCount -> [kafka, server, ReplicaManager, LeaderCount]
   *  default.General.api_drop_table_15min_rate -> [default, General, api, drop, table, 15min, rate]
   * @param metricName
   * @return
   */
  private String[] getIndidivualSplits(String metricName) {
    List<String> tokens = new ArrayList<>();
    String[] splits = new String[0];
    if (metricName.contains(".")) {
      splits = metricName.split("\\.");
      for (String split : splits) {
        if (split.contains("_")) {
          String[] subSplits = split.split("\\_");
          tokens.addAll(Arrays.asList(subSplits));
        } else {
          tokens.add(split);
        }
      }
    }

    if (splits.length <= 1) {
      splits = metricName.split("\\_");
      return splits;
    }

    if (splits.length <= 1) {
      splits = metricName.split("\\=");
      return splits;
    }

    return tokens.toArray(new String[tokens.size()]);
  }

  /**
   * Stem the metric name. Remove a set of usual suspects characters.
   * @param metricName
   * @return
   */
  private String stem(String metricName) {
    String metric = metricName.toLowerCase();
    String regex = "[\\.\\_\\%\\-\\=\\/\\@\\(\\)\\[\\]\\:]";
    String trimmedMetric = StringUtils.removePattern(metric, regex);
    return trimmedMetric;
  }


  /**
   * Computes the UUID of a string. (hostname)
   * Uses the ascii sum of the String. Numbers in the String are treated as actual numerical values rather than ascii values.
   * @param value
   * @param maxLength
   * @return byte array of length 'maxlength'
   */
  @Override
  protected byte[] computeUuidInternal(String value, int maxLength) {

    if (StringUtils.isEmpty(value)) {
      return null;
    }

    int customAsciiSum = 27177289 + (int) computeWeightedNumericalAsciiSum(value); //seed = 1489

    StringBuilder customAsciiSumStr = new StringBuilder(String.valueOf(customAsciiSum));
    if (customAsciiSumStr.length() < maxLength) {
      String originalcustomAsciiSumStr = customAsciiSumStr.toString();
      while (customAsciiSumStr.length() < maxLength) {
        customAsciiSumStr.append(originalcustomAsciiSumStr);
      }
      return customAsciiSumStr.substring(0, maxLength).getBytes();
    } else {
      return customAsciiSumStr.substring(customAsciiSumStr.length() - maxLength, customAsciiSumStr.length()).getBytes();
    }
  }

  public long computeWeightedNumericalAsciiSum(String value) {
    int len = value.length();
    long numericValue = 0;
    long sum = 0;
    int numericCtr = 0;
    for (int i = 0; i < len;) {
      int ascii = value.charAt(i);
      if (48 <= ascii && ascii <= 57 && numericCtr < 4) {
        numericValue = numericValue * 10 + (ascii - 48);
        numericCtr++;
        i++;
      } else {
        if (numericValue > 0) {
          sum += numericValue;
          numericValue = 0;
        }
        if (numericCtr < 4) {
          sum += value.charAt(i);
          i++;
        }
        numericCtr = 0;
      }
    }

    if (numericValue != 0) {
      sum +=numericValue;
    }
    return sum;
  }
}