/*
 * 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.zookeeper;

import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.regex.Pattern;
import org.apache.zookeeper.common.StringUtils;

/**
 * a class that represents the stats associated with quotas
 */
public class StatsTrack {

    private static final String countStr = "count";
    private static final String countHardLimitStr = "countHardLimit";

    private static final String byteStr = "bytes";
    private static final String byteHardLimitStr = "byteHardLimit";

    private final Map<String, Long> stats = new HashMap<>();
    private static final Pattern PAIRS_SEPARATOR = Pattern.compile("[,;]+");

    /**
     * a default constructor for
     * stats
     */
    public StatsTrack() {
        this("");
    }

    /**
     *
     * @param stat the byte[] stat to be initialized with
     */
    public StatsTrack(byte[] stat) {
        this(new String(stat, StandardCharsets.UTF_8));
    }

    /**
     * the stat string should be of the form key1str=long,key2str=long,..
     * where either , or ; are valid separators
     * uninitialized values are returned as -1
     * @param stat the stat string to be initialized with
     */
    public StatsTrack(String stat) {
        this.stats.clear();
        if (stat == null || stat.length() == 0) {
            return;
        }
        String[] keyValuePairs = PAIRS_SEPARATOR.split(stat);
        for (String keyValuePair : keyValuePairs) {
            String[] kv = keyValuePair.split("=");
            this.stats.put(kv[0], Long.parseLong(StringUtils.isEmpty(kv[1]) ? "-1" : kv[1]));
        }
    }


    /**
     * get the count of nodes allowed as part of quota
     *
     * @return the count as part of this string
     */
    public long getCount() {
        return getValue(countStr);
    }

    /**
     * set the count for this stat tracker.
     *
     * @param count
     *            the count to set with
     */
    public void setCount(long count) {
        setValue(countStr, count);
    }

    /**
     * get the count of nodes allowed as part of quota (hard limit)
     *
     * @return the count as part of this string
     */
    public long getCountHardLimit() {
        return getValue(countHardLimitStr);
    }

    /**
     * set the count hard limit
     *
     * @param count the count limit to set
     */
    public void setCountHardLimit(long count) {
        setValue(countHardLimitStr, count);
    }

    /**
     * get the count of bytes allowed as part of quota
     *
     * @return the bytes as part of this string
     */
    public long getBytes() {
        return getValue(byteStr);
    }

    /**
     * set the bytes for this stat tracker.
     *
     * @param bytes
     *            the bytes to set with
     */
    public void setBytes(long bytes) {
        setValue(byteStr, bytes);
    }

    /**
     * get the count of bytes allowed as part of quota (hard limit)
     *
     * @return the bytes as part of this string
     */
    public long getByteHardLimit() {
        return getValue(byteHardLimitStr);
    }

    /**
     * set the byte hard limit
     *
     * @param bytes the byte limit to set
     */
    public void setByteHardLimit(long bytes) {
        setValue(byteHardLimitStr, bytes);
    }

    /**
     * get helper to lookup a given key
     *
     * @param key the key to lookup
     * @return key's value or -1 if it doesn't exist
     */
    private long getValue(String key) {
        Long val = this.stats.get(key);
        return val == null ? -1 : val.longValue();
    }

    /**
     * set helper to set the value for the specified key
     *
     * @param key   the key to set
     * @param value the value to set
     */
    private void setValue(String key, long value) {
        this.stats.put(key, value);
    }

    /*
     * returns the string that maps to this stat tracking.
     *
     * Builds a string of the form
     * "count=4,bytes=5=;countHardLimit=10;byteHardLimit=10"
     *
     * This string is slightly hacky to preserve compatibility with 3.4.3 and
     * older parser. In particular, count must be first, bytes must be second,
     * all new fields must use a separator that is not a "," (so, ";"), and the
     * seemingly spurious "=" after the bytes field is essential to allowing
     * it to be parseable by the old parsing code.
     */
    @Override
    public String toString() {
        StringBuilder buf = new StringBuilder();
        ArrayList<String> keys = new ArrayList<>(stats.keySet());

        // Special handling for count=,byte= to enforce them coming first
        // for backwards compatibility
        keys.remove(countStr);
        keys.remove(byteStr);
        buf.append(countStr);
        buf.append("=");
        buf.append(getCount());
        buf.append(",");
        buf.append(byteStr);
        buf.append("=");
        buf.append(getBytes());
        if (!keys.isEmpty()) {
            // Add extra = to trick old parsing code so it will ignore new flags
            buf.append("=");
            Collections.sort(keys);
            for (String key : keys) {
                buf.append(";");
                buf.append(key);
                buf.append("=");
                buf.append(stats.get(key));
            }
        }
        return buf.toString();
    }

    public byte[] getStatsBytes() {
        return toString().getBytes(StandardCharsets.UTF_8);
    }

    @Override
    public boolean equals(final Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        final StatsTrack that = (StatsTrack) o;
        return Objects.equals(stats, that.stats);
    }

    @Override
    public int hashCode() {
        return Objects.hash(stats);
    }
}
