package io.vertx.ext.web.impl;

import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;

import io.vertx.codegen.annotations.Nullable;
import io.vertx.ext.web.ParsedHeaderValue;

public class ParsableHeaderValue implements ParsedHeaderValue {

  private final String headerContent;

  protected String value;
  private float weight;

  private Map<String, String> parameter;

  private int paramsWeight;


  public ParsableHeaderValue(String headerContent) {
    Objects.requireNonNull(headerContent, "headerContent must not be null");
    this.headerContent = headerContent;
    value = null;
    weight = -1;
    parameter = Collections.emptyMap();
  }

  @Override
  public String rawValue() {
    return headerContent;
  }

  @Override
  public String value() {
    ensureHeaderProcessed();
    return value;
  }

  @Override
  public float weight() {
    ensureHeaderProcessed();
    return weight;
  }

  @Override
  public boolean isPermitted() {
    ensureHeaderProcessed();
    // rfc7231 states the least possible number above 0 is 0.001
    return weight < 0.001;
  }

  public String parameter(String key) {
    ensureHeaderProcessed();
    return parameter.get(key);
  }

  public Map<String, String> parameters() {
    ensureHeaderProcessed();
    return Collections.unmodifiableMap(parameter);
  }

  public final boolean isMatchedBy(ParsedHeaderValue matchTry){
    ParsableHeaderValue impl = (ParsableHeaderValue) matchTry;
    return this.headerContent.equals(impl.headerContent) || isMatchedBy2(impl);
  }

  protected boolean isMatchedBy2(ParsableHeaderValue matchTry){
    ensureHeaderProcessed();
    if (matchTry.parameter.isEmpty()) {
      return true;
    }
    if (parameter.isEmpty()) {
      return false;
    }

    for (Entry<String, String> requiredParameter : matchTry.parameter.entrySet()) {
      String parameterValueToTest = parameter.get(requiredParameter.getKey());
      String requiredParamVal = requiredParameter.getValue();
      if (parameterValueToTest == null || (
          !requiredParamVal.isEmpty() && !requiredParamVal.equalsIgnoreCase(parameterValueToTest))
         ){
        return false;
      }
    }
    return true;

  }

  @Override
  public <T extends ParsedHeaderValue> @Nullable T findMatchedBy(Collection<T> matchTries) {

    for (T matchTry : matchTries) {
      if(isMatchedBy(matchTry)){
        return matchTry;
      }
    }
    return null;
  }

  private void ensureParameterIsHashMap() {
    if(parameter.isEmpty()){
      parameter = new HashMap<>();
    }
  }

  protected void ensureHeaderProcessed() {
    if(weight < 0){
      // as per rfc7231, the default value is 1
      weight = DEFAULT_WEIGHT;
      HeaderParser.parseHeaderValue(
          headerContent,
          this::setValue,
          this::setWeight,
          this::addParameter
      );
      paramsWeight = parameter.isEmpty() ? 0 : 1;
    }
  }

  public ParsableHeaderValue forceParse(){
    ensureHeaderProcessed();
    return this;
  }

  private void setValue(String value) {
    this.value = value;
  }
  private void addParameter(String key, String value) {
    ensureParameterIsHashMap();
    if(value == null){
      value = "";
      paramsWeight = Math.max(1, paramsWeight);
    } else {
      paramsWeight = Math.max(2, paramsWeight);
    }
    parameter.put(key, value);
  }
  private void setWeight(float weight) {
    // Keep between 0 and 1 while dropping after the 3rd digit to the right (rfc7231#section-5.3.1)
    this.weight = ((int)((Math.max(0, Math.min(1, weight)) * 100)) / 100.0f);
  }

  public final int weightedOrder(){
    ensureHeaderProcessed();
    return (int)(weight() * 1000) + (weightedOrderPart2() * 10) + paramsWeight;
  }

  protected int weightedOrderPart2(){
    return 0;
  }

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

  @Override
  public boolean equals(Object obj) {
    if (this == obj) {
      return true;
    }
    if (!(obj instanceof ParsableHeaderValue)) {
      return false;
    }
    ParsableHeaderValue other = (ParsableHeaderValue) obj;
    if (headerContent == null) {
      return other.headerContent == null;
    } else {
      return headerContent.equals(other.headerContent);
    }
  }

}
