/**
 * Copyright 2013 OpenSocial Foundation
 * Copyright 2013 International Business Machines Corporation
 *
 * 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.
 *
 * Utility library for working with Activity Streams Actions
 * Requires underscorejs.
 *
 * @author James M Snell ([email protected])
 */
package com.ibm.common.activitystreams.actions;

import static com.ibm.common.activitystreams.Makers.nlv;
import static com.ibm.common.activitystreams.util.Util.DEFAULT_LOCALE;

import java.io.Serializable;
import java.util.Map;
import java.util.regex.Pattern;

import com.google.common.base.Objects;
import com.google.common.base.Supplier;
import com.google.common.collect.BoundType;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;
import com.google.common.collect.Ordering;
import com.google.common.collect.Range;
import com.ibm.common.activitystreams.Makers;
import com.ibm.common.activitystreams.NLV;
import com.ibm.common.activitystreams.ValueType;
import com.ibm.common.activitystreams.NLV.MapNLV;
import com.ibm.common.activitystreams.NLV.SimpleNLV;
import com.ibm.common.activitystreams.util.AbstractWritable;

@SuppressWarnings("unchecked")
public class Parameter 
  extends AbstractWritable
  implements ParameterValue {
    
  /**
   * Method makeParameter.
   * @return Builder
   */
  public static Builder makeParameter() {
    return new Builder();
  }

  public static class Builder
    extends AbstractWritable.AbstractWritableBuilder<Parameter,Builder> {

    protected Builder() {
      writeUsing(ActionMakers.io);
    }
    
    private final Map<String,Object> map = 
      Maps.newHashMap();
    
    public Builder language(String lang) {
      if (lang != null)
        map.put("language", lang);
      else 
        map.remove("language");
      return this;
    }
    
    protected Builder _nlv(String key, String value) {
      if (value != null)
         map.put(key, nlv(value));
      else 
         map.remove(key);
       return this;
    }
  
    protected Builder _nlv(String key, NLV nlv) {
      if (nlv != null)  
        map.put(key, nlv);
      else 
        map.remove(key);
      return this;
    }
  
    protected Builder _nlv(String key, Supplier<? extends NLV> nlv) {
      return _nlv(key,nlv.get());
    }
  
    protected Builder _nlv(String key, Map<String,String> map) {
      if (map != null)
        for (Map.Entry<String,String> entry : map.entrySet())
          _nlv(key,entry.getKey(),entry.getValue());
      else 
        this.map.remove(key);
      return this;
    }
    
    protected Builder _nlv(String key, String lang, String value) {
      if (map.containsKey(key)) {
        Object obj = map.get(key);
        if (obj instanceof NLV) {
          NLV nlv = (NLV) obj;
          switch(nlv.valueType()) {
          case SIMPLE:
            String l = (String) map.get("language");
            if (l == null)
              l = DEFAULT_LOCALE;
            NLV.MapNLV.Builder b = 
              Makers.nlv();
            if (lang.equals(l))
              b.set(lang, value);
            else
              b.set(l, ((NLV.SimpleNLV)obj).value())
               .set(lang, value);
            map.put(key, b);
            return this;
          case OBJECT: 
            map.put(key, 
              Makers.nlv()
                .from((NLV.MapNLV)obj, lang)
                .set(lang, value));
            return this;
          default:
            throw new IllegalArgumentException();
          }
        } else if (obj instanceof NLV.MapNLV.Builder) {
          ((NLV.MapNLV.Builder) obj).set(lang, value);
          return this;
        }
      }
      map.put(key, Makers.nlv().set(lang,value));      
      return this;
    }
    
    public Builder displayName(String val) {
      return _nlv("displayName",val);
    }
    
    public Builder displayName(NLV nlv) {
      return _nlv("displayName", nlv);
    }
    
    public Builder displayName(Supplier<? extends NLV> nlv) {
      return _nlv("displayName", nlv);
    }
    
    public Builder displayName(String lang, String val) {
      return _nlv("displayName", lang, val);
    }
    
    public Builder placeholder(String placeholder) {
      return _nlv("placeholder", placeholder);
    }
    
    public Builder placeholder(NLV placeholder) {
      return _nlv("placeholder", placeholder);
    }

    public Builder placeholder(Supplier<? extends NLV> nlv) {
      return _nlv("placeholder", nlv);
    }
    
    public Builder placeholder(String lang, String val) {
      return _nlv("placeholder", lang, val);
    }
    
    public Builder type(String type) {
      if (type != null)
        map.put("type", type);
      else 
        map.remove("type");
      return this;
    }
    
    public Builder required() {
      map.remove("required");
      return this;
    }
    
    public Builder optional() {
      map.put("required", false);
      return this;
    }

    public Builder repeated() {
      map.put("repeated", true);
      return this;
    }
    
    public Builder notRepeated() {
      map.remove("repeated");
      return this;
    }
    
    public Builder value(Object value) {
      if (value != null)
        map.put("value", value);
      else 
        map.remove("value");
      return this;
    }
    
    public Builder defaultValue(Object value) {
      if (value != null)
        map.put("default", value);
      else 
        map.remove("default");
      return this;
    }
        
    public Builder pattern(Pattern... pattern) {
      if (pattern != null && pattern.length > 0) {
        ImmutableSet.Builder<String> patterns = 
          ImmutableSet.builder();
        for (Pattern p : pattern)
          patterns.add(p.pattern());
        map.put("pattern",patterns.build());
      } else {
        map.remove("pattern");
      }
      return this;
    }

    public Builder pattern(String... pattern) {
      if (pattern != null && pattern.length > 0) {
        ImmutableSet<String> patterns = 
          ImmutableSet.copyOf(pattern);
        map.put("pattern",patterns);
      } else {
        map.remove("pattern");
      }
      return this;
    }
    
    public Builder pattern(Iterable<String> patterns) {
      if (patterns != null && Iterables.size(patterns) > 0) {
        map.put("pattern", ImmutableSet.copyOf(patterns));
      } else 
        map.remove("pattern");
      return this;
    }
    
    public Builder minInclusive(Object min) {
      if (min != null)
        map.put("minInclusive", min);
      else 
        map.remove("minInclusive");
      return this;
    }
    
    public Builder minExclusive(Object min) {
      if (min != null)
        map.put("minExclusive", min);
      else 
        map.remove("minExclusive");
      return this;
    }
    
    public Builder maxInclusive(Object max) {
      if (max != null)
        map.put("maxInclusive", max);
      else 
        map.remove("maxInclusive");
      return this;
    }
    
    public Builder maxExclusive(Object max) {
      if (max != null)
        map.put("maxExclusive", max);
      else
        map.remove("maxExclusive");
      return this;
    }
    
    public Builder bound(Range<?> range) {
      if (range != null) {
        if (range.hasLowerBound()) {
          switch(range.lowerBoundType()) {
          case CLOSED:
            minInclusive(range.lowerEndpoint());
            break;
          case OPEN:
            minExclusive(range.lowerEndpoint());
            break;
          default:
            break;
          }
        } else {
          minInclusive(null);
          minExclusive(null);
        }
        if (range.hasUpperBound()) {
          switch(range.upperBoundType()) {
          case CLOSED:
            maxInclusive(range.upperEndpoint());
            break;
          case OPEN:
            maxExclusive(range.upperEndpoint());
            break;
          default:
            break;
          }
        } else {
          maxInclusive(null);
          maxExclusive(null);
        }
      }
      return this;
    }
        
    public Builder step(Number step) {
      if (step != null)
        map.put("step", step);
      else
        map.remove("step");
      return this;
    }

    public Builder enumeration(Object... vals) {
      if (vals != null && vals.length > 0)
        map.put("enumeration", ImmutableList.copyOf(vals));
      else 
        map.remove("enumeration");
      return this;
    }
    
    public Builder maxLength(int length) {
      if (length > -1)
        map.put("maxLength", length);
      else
        map.remove("maxLength");
      return this;
    }
    
    public Builder minLength(int length) {
      if (length > -1)
        map.put("minLength", length);
      else
        map.remove("minLength");
      return this;
    }
    
    public Builder totalDigits(int num) {
      if (num > -1)
        map.put("totalDigits", num);
      else 
        map.remove("totalDigits");
      return this;
    }
    
    public Builder fractionDigits(int num) {
      if (num > -1) 
        map.put("fractionDigits", num);
      else 
        map.remove("fractionDigits");
      return this;
    }
    
    public Parameter get() {
      return new Parameter(this);
    }

  }
  
  private final ImmutableMap<String,Object> map;
  
  /**
   * Constructor for Parameter.
   * @param builder Builder
   */
  protected Parameter(Builder builder) {
    super(builder);
    this.map = ImmutableMap.copyOf(builder.map);
  }
  
  /**
   * Method required.
   * @return boolean
   */
  public boolean required() {
    return !has("required") ? 
      true : (Boolean)map.get("required");
  }
  
  /**
   * Method repeated.
   * @return boolean
   */
  public boolean repeated() {
    return !has("repeated") ?
      false : (Boolean)map.get("repeated");
  }
  
  /**
   * Method value.
   * @return O
   */
  public <O>O value() {
    return (O)map.get("value");
  }
  
  /**
   * Method value.
   * @param defaultValue O
   * @return O
   */
  public <O>O value(O defaultValue) {
    O val = value();
    return val != null ? val : defaultValue;
  }
  
  /**
   * Method defaultValue.
   * @return O
   */
  public <O>O defaultValue() {
    return (O)map.get("default");
  }
  
  /**
   * Method defaultValue.
   * @param defaultValue O
   * @return O
   */
  public <O>O defaultValue(O defaultValue) {
    O val = defaultValue();
    return val != null ? val : defaultValue;
  }
  
  public String type() {
    return (String)map.get("type");
  }
  
  /**
   * Method pattern.
   * @return String
   */
  public Iterable<String> pattern() {
    return (Iterable<String>)map.get("pattern");
  }
  
  public <O>O maxInclusive() {
    return (O)map.get("maxInclusive");
  }
  
  public <O>O maxExclusive() {
    return (O)map.get("maxExclusive");
  }
  
  public <O>O minInclusive() {
    return (O)map.get("minInclusive");
  }
  
  public <O>O minExclusive() {
    return (O)map.get("minExclusive");
  }
  
  public boolean has(String key) {
    return map.containsKey(key);
  }
  
  public boolean hasUpperBound() {
    return has("maxInclusive") || has("maxExclusive");
  }
  
  public <O extends Comparable<? super O>>Range<O> bounds() {
    O mini = minInclusive();
    O mine = minExclusive();
    O maxi = maxInclusive();
    O maxe = maxExclusive();
    Ordering<O> ordering = Ordering.<O>natural();
    O min = ordering.nullsLast().min(mini,mine);
    O max = ordering.nullsFirst().max(maxi,maxe);
    BoundType lower = 
      min == null ? null :
      min == mini ? BoundType.CLOSED :
        BoundType.OPEN;
    BoundType upper = 
      max == null ? null : 
      max == maxi ? BoundType.CLOSED :
        BoundType.OPEN;
    if (lower == null && upper == null)
      return Range.<O>all();
    else if (lower != null && upper == null) 
      return lower == BoundType.CLOSED ? 
        Range.atLeast(min) : 
        Range.greaterThan(min);
    else if (lower == null && upper != null)
      return upper == BoundType.CLOSED ?
        Range.atMost(max) :
        Range.lessThan(max);
    else {
      return Range.range(min, lower, max, upper);
    }
  }
  
  public <N extends Number>N step() {
    return (N)map.get("step");
  }
  
  public <N extends Number>N step(N defaultValue) {
    N n = (N)map.get("step");
    return n != null ? n : defaultValue;
  }
  
  /**
   * Method stepInt.
   * @return int
   */
  public int stepInt() {
    return step();
  }
  
  /**
   * Method stepInt.
   * @param defaultValue int
   * @return int
   */
  public int stepInt(int defaultValue) {
    return step(defaultValue);
  }
  
  /**
   * Method stepLong.
   * @return long
   */
  public long stepLong() {
    return step();
  }
  
  /**
   * Method getLong.
   * @param defaultValue long
   * @return long
   */
  public long stepLong(long defaultValue) {
    return step(defaultValue);
  }
  
  /**
   * Method stepShort.
   * @return short
   */
  public short stepShort() {
    return step();
  }
  
  /**
   * Method stepShort.
   * @param defaultValue short
   * @return short
   */
  public short stepShort(short defaultValue) {
    return step(defaultValue);
  }
  
  /**
   * Method stepDouble.
   * @return double
   */
  public double stepDouble() {
    return step();
  }
  
  /**
   * Method stepDouble.
   * @param defaultValue double
   * @return double
   */
  public double stepDouble(double defaultValue) {
    return step(defaultValue);
  }
  
  /**
   * Method stepFloat.
   * @return float
   */
  public float stepFloat() {
    return step();
  }
  
  /**
   * Method stepFloat.
   * @param defaultValue float
   * @return float
   */
  public float stepFloat(float defaultValue) {
    return step(defaultValue);
  }
  
  /**
   * Method enumVals.
   * @return Iterable<Object>
   */
  public Iterable<Object> enumeration() {
    if (has("enumeration"))
      return (Iterable<Object>)map.get("enumeration");
    else 
      return ImmutableSet.of();
  }
  
  /**
   * Method placeholder.
   * @return NLV
   */
  public NLV placeholder() {
    return (NLV)map.get("placeholder");
  }
  
  /**
   * Method placeholderString.
   * @return String
   */
  public String placeholderString() {
    return _nlv("placeholder");
  }
  
  /**
   * Method placeholderString.
   * @param lang String
   * @return String
   */
  public String placeholderString(String lang) {
    return _nlv("placeholder", lang);
  }
  
  public int maxLength() {
    Integer i = (Integer) map.get("maxLength");
    return i != null ? i : -1;
  }
  
  public int minLength() {
    Integer i = (Integer) map.get("minLength");
    return i != null ? i : -1;
  }
  
  public int totalDigits() {
    Integer i = (Integer) map.get("totalDigits");
    return i != null ? i : -1;
  }
  
  public int fractionDigits() {
    Integer i = (Integer) map.get("fractionDigits");
    return i != null ? i : -1;
  }
  
  /**
   * Method placeholder.
   * @return NLV
   */
  public NLV displayName() {
    return (NLV)map.get("displayName");
  }
  
  /**
   * Method placeholderString.
   * @return String
   */
  public String displayNameString() {
    return _nlv("displayName");
  }
  
  /**
   * Method placeholderString.
   * @param lang String
   * @return String
   */
  public String displayNameString(String lang) {
    return _nlv("displayName", lang);
  }
  
  public String language() {
    return (String)map.get("language");
  }
  
  /**
   * Method _nlv.
   * @param key String
   * @return String 
   **/
  protected String _nlv(String key) {
    String lang = language();
    return _nlv(key, lang != null ? lang : DEFAULT_LOCALE);
  }
  
  /**
   * Method _nlv.
   * @param key String
   * @param lang String
   * @return String 
   **/
  protected String _nlv(String key, String lang) {
    NLV nlv = 
      (NLV)map.get(key);
    switch(nlv.valueType()) {
    case SIMPLE:
      NLV.SimpleNLV sim = 
        (SimpleNLV) nlv;
      String l = language();
      return l == null || Objects.equal(l,lang) ? sim.value() : null;
    case OBJECT:
      NLV.MapNLV map = 
       (MapNLV) nlv;
      return map.value(lang);
    default:
      return null;
    } 
  }
  
  public ValueType valueType() {
    return ValueType.OBJECT;
  }
  
  Object writeReplace() throws java.io.ObjectStreamException {
    return new SerializedForm(this);
  }
  
  private static class SerializedForm 
  implements Serializable {
    private static final long serialVersionUID = -1975376657749952999L;
    private ImmutableMap<String,Object> map;
    SerializedForm(Parameter obj) {
      this.map = obj.map;
    }
  
    Object readResolve() 
      throws java.io.ObjectStreamException {
        Parameter.Builder builder = new Parameter.Builder();
        builder.map.putAll(map);
        return builder.get();
    }

  }
}