package biz.k11i.xgboost;

import biz.k11i.xgboost.config.PredictorConfiguration;
import biz.k11i.xgboost.gbm.GradBooster;
import biz.k11i.xgboost.learner.ObjFunction;
import biz.k11i.xgboost.spark.SparkModelParam;
import biz.k11i.xgboost.util.FVec;
import biz.k11i.xgboost.util.ModelReader;

import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;

/**
 * Predicts using the Xgboost model.
 */
public class Predictor implements Serializable {
    private ModelParam mparam;
    private SparkModelParam sparkModelParam;
    private String name_obj;
    private String name_gbm;
    private ObjFunction obj;
    private GradBooster gbm;

    public Predictor(InputStream in) throws IOException {
        this(in, null);
    }

    /**
     * Instantiates with the Xgboost model
     *
     * @param in input stream
     * @param configuration configuration
     * @throws IOException If an I/O error occurs
     */
    public Predictor(InputStream in, PredictorConfiguration configuration) throws IOException {
        if (configuration == null) {
            configuration = PredictorConfiguration.DEFAULT;
        }

        ModelReader reader = new ModelReader(in);

        readParam(reader);
        initObjFunction(configuration);
        initObjGbm();

        gbm.loadModel(configuration, reader, mparam.saved_with_pbuffer != 0);
    }

    void readParam(ModelReader reader) throws IOException {
        byte[] first4Bytes = reader.readByteArray(4);
        byte[] next4Bytes = reader.readByteArray(4);

        float base_score;
        int num_feature;

        if (first4Bytes[0] == 0x62 &&
                first4Bytes[1] == 0x69 &&
                first4Bytes[2] == 0x6e &&
                first4Bytes[3] == 0x66) {

            // Old model file format has a signature "binf" (62 69 6e 66)
            base_score = reader.asFloat(next4Bytes);
            num_feature = reader.readUnsignedInt();

        } else if (first4Bytes[0] == 0x00 &&
                first4Bytes[1] == 0x05 &&
                first4Bytes[2] == 0x5f) {

            // Model generated by xgboost4j-spark?
            String modelType = null;
            if (first4Bytes[3] == 0x63 &&
                    next4Bytes[0] == 0x6c &&
                    next4Bytes[1] == 0x73 &&
                    next4Bytes[2] == 0x5f) {
                // classification model
                modelType = SparkModelParam.MODEL_TYPE_CLS;

            } else if (first4Bytes[3] == 0x72 &&
                    next4Bytes[0] == 0x65 &&
                    next4Bytes[1] == 0x67 &&
                    next4Bytes[2] == 0x5f) {
                // regression model
                modelType = SparkModelParam.MODEL_TYPE_REG;
            }

            if (modelType != null) {
                int len = (next4Bytes[3] << 8) + (reader.readByteAsInt());
                String featuresCol = reader.readUTF(len);

                this.sparkModelParam = new SparkModelParam(modelType, featuresCol, reader);

                base_score = reader.readFloat();
                num_feature = reader.readUnsignedInt();

            } else {
                base_score = reader.asFloat(first4Bytes);
                num_feature = reader.asUnsignedInt(next4Bytes);
            }

        } else {
            base_score = reader.asFloat(first4Bytes);
            num_feature = reader.asUnsignedInt(next4Bytes);
        }

        mparam = new ModelParam(base_score, num_feature, reader);

        name_obj = reader.readString();
        name_gbm = reader.readString();
    }

    void initObjFunction(PredictorConfiguration configuration) {
        obj = configuration.getObjFunction();

        if (obj == null) {
            obj = ObjFunction.fromName(name_obj);
        }
    }

    void initObjGbm() {
        obj = ObjFunction.fromName(name_obj);
        gbm = GradBooster.Factory.createGradBooster(name_gbm);
        gbm.setNumClass(mparam.num_class);
    }

    /**
     * Generates predictions for given feature vector.
     *
     * @param feat feature vector
     * @return prediction values
     */
    public float[] predict(FVec feat) {
        return predict(feat, false);
    }

    /**
     * Generates predictions for given feature vector.
     *
     * @param feat          feature vector
     * @param output_margin whether to only predict margin value instead of transformed prediction
     * @return prediction values
     */
    public float[] predict(FVec feat, boolean output_margin) {
        return predict(feat, output_margin, 0);
    }

    /**
     * Generates predictions for given feature vector.
     *
     * @param feat          feature vector
     * @param output_margin whether to only predict margin value instead of transformed prediction
     * @param ntree_limit   limit the number of trees used in prediction
     * @return prediction values
     */
    public float[] predict(FVec feat, boolean output_margin, int ntree_limit) {
        float[] preds = predictRaw(feat, ntree_limit);
        if (! output_margin) {
            preds = obj.predTransform(preds);
        }
        return preds;
    }

    float[] predictRaw(FVec feat, int ntree_limit) {
        float[] preds = gbm.predict(feat, ntree_limit);
        for (int i = 0; i < preds.length; i++) {
            preds[i] += mparam.base_score;
        }
        return preds;
    }

    /**
     * Generates a prediction for given feature vector.
     *

     * This method only works when the model outputs single value.
     *

     *
     * @param feat feature vector
     * @return prediction value
     */
    public float predictSingle(FVec feat) {
        return predictSingle(feat, false);
    }

    /**
     * Generates a prediction for given feature vector.
     *

     * This method only works when the model outputs single value.
     *

     *
     * @param feat          feature vector
     * @param output_margin whether to only predict margin value instead of transformed prediction
     * @return prediction value
     */
    public float predictSingle(FVec feat, boolean output_margin) {
        return predictSingle(feat, output_margin, 0);
    }

    /**
     * Generates a prediction for given feature vector.
     *

     * This method only works when the model outputs single value.
     *

     *
     * @param feat          feature vector
     * @param output_margin whether to only predict margin value instead of transformed prediction
     * @param ntree_limit   limit the number of trees used in prediction
     * @return prediction value
     */
    public float predictSingle(FVec feat, boolean output_margin, int ntree_limit) {
        float pred = predictSingleRaw(feat, ntree_limit);
        if (!output_margin) {
            pred = obj.predTransform(pred);
        }
        return pred;
    }

    float predictSingleRaw(FVec feat, int ntree_limit) {
        return gbm.predictSingle(feat, ntree_limit) + mparam.base_score;
    }

    /**
     * Predicts leaf index of each tree.
     *
     * @param feat feature vector
     * @return leaf indexes
     */
    public int[] predictLeaf(FVec feat) {
        return predictLeaf(feat, 0);
    }

    /**
     * Predicts leaf index of each tree.
     *
     * @param feat        feature vector
     * @param ntree_limit limit
     * @return leaf indexes
     */
    public int[] predictLeaf(FVec feat,
                             int ntree_limit) {
        return gbm.predictLeaf(feat, ntree_limit);
    }

    public SparkModelParam getSparkModelParam() {
        return sparkModelParam;
    }

    /**
     * Returns number of class.
     *
     * @return number of class
     */
    public int getNumClass() {
        return mparam.num_class;
    }

    /**
     * Parameters.
     */
    static class ModelParam implements Serializable {
        /* \brief global bias */
        final float base_score;
        /* \brief number of features  */
        final /* unsigned */ int num_feature;
        /* \brief number of class, if it is multi-class classification  */
        final int num_class;
        /*! \brief whether the model itself is saved with pbuffer */
        final int saved_with_pbuffer;
        /*! \brief reserved field */
        final int[] reserved;

        ModelParam(float base_score, int num_feature, ModelReader reader) throws IOException {
            this.base_score = base_score;
            this.num_feature = num_feature;
            this.num_class = reader.readInt();
            this.saved_with_pbuffer = reader.readInt();
            this.reserved = reader.readIntArray(30);
        }
    }

    public GradBooster getBooster(){
        return gbm;
    }
}


package biz.k11i.xgboost.learner;

import biz.k11i.xgboost.config.PredictorConfiguration;
import net.jafama.FastMath;

import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;

/**
 * Objective function implementations.
 */
public class ObjFunction implements Serializable {
    private static final Map<String, ObjFunction> FUNCTIONS = new HashMap<>();

    static {
        register("rank:pairwise", new ObjFunction());
        register("binary:logistic", new RegLossObjLogistic());
        register("binary:logitraw", new ObjFunction());
        register("multi:softmax", new SoftmaxMultiClassObjClassify());
        register("multi:softprob", new SoftmaxMultiClassObjProb());
        register("reg:linear", new ObjFunction());
    }

    /**
     * Gets {@link ObjFunction} from given name.
     *
     * @param name name of objective function
     * @return objective function
     */
    public static ObjFunction fromName(String name) {
        ObjFunction result = FUNCTIONS.get(name);
        if (result == null) {
            throw new IllegalArgumentException(name + " is not supported objective function.");
        }
        return result;
    }

    /**
     * Register an {@link ObjFunction} for a given name.
     *
     * @param name name of objective function
     * @param objFunction objective function
     * @deprecated This method will be made private. Please use {@link PredictorConfiguration.Builder#objFunction(ObjFunction)} instead.
     */
    public static void register(String name, ObjFunction objFunction) {
        FUNCTIONS.put(name, objFunction);
    }

    /**
     * Uses Jafama's {@link FastMath#exp(double)} instead of {@link Math#exp(double)}.
     *
     * @param useJafama {@code true} if you want to use Jafama's {@link FastMath#exp(double)},
     *                  or {@code false} if you don't want to use it but JDK's {@link Math#exp(double)}.
     */
    public static void useFastMathExp(boolean useJafama) {
        if (useJafama) {
            register("binary:logistic", new RegLossObjLogistic_Jafama());
            register("multi:softprob", new SoftmaxMultiClassObjProb_Jafama());

        } else {
            register("binary:logistic", new RegLossObjLogistic());
            register("multi:softprob", new SoftmaxMultiClassObjProb());
        }
    }

    /**
     * Transforms prediction values.
     *
     * @param preds prediction
     * @return transformed values
     */
    public float[] predTransform(float[] preds) {
        // do nothing
        return preds;
    }

    /**
     * Transforms a prediction value.
     *
     * @param pred prediction
     * @return transformed value
     */
    public float predTransform(float pred) {
        // do nothing
        return pred;
    }

    /**
     * Logistic regression.
     */
    static class RegLossObjLogistic extends ObjFunction {
        @Override
        public float[] predTransform(float[] preds) {
            for (int i = 0; i < preds.length; i++) {
                preds[i] = sigmoid(preds[i]);
            }
            return preds;
        }

        @Override
        public float predTransform(float pred) {
            return sigmoid(pred);
        }

        float sigmoid(float x) {
            return (1f / (1f + (float) Math.exp(-x)));
        }
    }

    /**
     * Logistic regression.
     *

     * Jafama's {@link FastMath#exp(double)} version.
     *

     */
    static class RegLossObjLogistic_Jafama extends RegLossObjLogistic {
        double sigmoid(double x) {
            return (1 / (1 + FastMath.exp(-x)));
        }
    }

    /**
     * Multiclass classification.
     */
    static class SoftmaxMultiClassObjClassify extends ObjFunction {
        @Override
        public float[] predTransform(float[] preds) {
            int maxIndex = 0;
            float max = preds[0];
            for (int i = 1; i < preds.length; i++) {
                if (max < preds[i]) {
                    maxIndex = i;
                    max = preds[i];
                }
            }

            return new float[]{maxIndex};
        }

        @Override
        public float predTransform(float pred) {
            throw new UnsupportedOperationException();
        }
    }

    /**
     * Multiclass classification (predicted probability).
     */
    static class SoftmaxMultiClassObjProb extends ObjFunction {
        @Override
        public float[] predTransform(float[] preds) {
            float max = preds[0];
            for (int i = 1; i < preds.length; i++) {
                max = Math.max(preds[i], max);
            }

            double sum = 0;
            for (int i = 0; i < preds.length; i++) {
                preds[i] = exp(preds[i] - max);
                sum += preds[i];
            }

            for (int i = 0; i < preds.length; i++) {
                preds[i] /= (float) sum;
            }

            return preds;
        }

        @Override
        public float predTransform(float pred) {
            throw new UnsupportedOperationException();
        }

        float exp(float x) {
            return (float) Math.exp(x);
        }
    }

    /**
     * Multiclass classification (predicted probability).
     *

     * Jafama's {@link FastMath#exp(double)} version.
     *

     */
    static class SoftmaxMultiClassObjProb_Jafama extends SoftmaxMultiClassObjProb {
        @Override
        float exp(float x) {
            return (float) FastMath.exp(x);
        }
    }
}


package biz.k11i.xgboost.tree;

import ai.h2o.algos.tree.INodeStat;
import biz.k11i.xgboost.util.FVec;
import biz.k11i.xgboost.util.ModelReader;

import java.io.IOException;
import java.io.Serializable;

/**
 * Regression tree.
 */
public class RegTreeImpl implements RegTree {
    private Param param;
    private Node[] nodes;
    private RTreeNodeStat[] stats;

    /**
     * Loads model from stream.
     *
     * @param reader input stream
     * @throws IOException If an I/O error occurs
     */
    public void loadModel(ModelReader reader) throws IOException {
        param = new Param(reader);

        nodes = new Node[param.num_nodes];
        for (int i = 0; i < param.num_nodes; i++) {
            nodes[i] = new Node(reader);
        }

        stats = new RTreeNodeStat[param.num_nodes];
        for (int i = 0; i < param.num_nodes; i++) {
            stats[i] = new RTreeNodeStat(reader);
        }
    }

    /**
     * Retrieves nodes from root to leaf and returns leaf index.
     *
     * @param feat    feature vector
     * @param root_id starting root index
     * @return leaf index
     */
    @Override
    public int getLeafIndex(FVec feat, int root_id) {
        int pid = root_id;

        Node n;
        while (!(n = nodes[pid])._isLeaf) {
            pid = n.next(feat);
        }

        return pid;
    }

    /**
     * Retrieves nodes from root to leaf and returns leaf value.
     *
     * @param feat    feature vector
     * @param root_id starting root index
     * @return leaf value
     */
    @Override
    public float getLeafValue(FVec feat, int root_id) {
        Node n = nodes[root_id];
        while (!n._isLeaf) {
            n = nodes[n.next(feat)];
        }

        return n.leaf_value;
    }

    @Override
    public Node[] getNodes() {
        return nodes;
    }

    RTreeNodeStat[] getStats() {
        return stats;
    }

    /**
     * Parameters.
     */
    static class Param implements Serializable {
        /*! \brief number of start root */
        final int num_roots;
        /*! \brief total number of nodes */
        final int num_nodes;
        /*!\brief number of deleted nodes */
        final int num_deleted;
        /*! \brief maximum depth, this is a statistics of the tree */
        final int max_depth;
        /*! \brief  number of features used for tree construction */
        final int num_feature;
        /*!
         * \brief leaf vector size, used for vector tree
         * used to store more than one dimensional information in tree
         */
        final int size_leaf_vector;
        /*! \brief reserved part */
        final int[] reserved;

        Param(ModelReader reader) throws IOException {
            num_roots = reader.readInt();
            num_nodes = reader.readInt();
            num_deleted = reader.readInt();
            max_depth = reader.readInt();
            num_feature = reader.readInt();

            size_leaf_vector = reader.readInt();
            reserved = reader.readIntArray(31);
        }
    }

    public static class Node extends RegTreeNode implements Serializable {
        // pointer to parent, highest bit is used to
        // indicate whether it's a left child or not
        final int parent_;
        // pointer to left, right
        final int cleft_, cright_;
        // split feature index, left split or right split depends on the highest bit
        final /* unsigned */ int sindex_;
        // extra info (leaf_value or split_cond)
        final float leaf_value;
        final float split_cond;

        private final int _defaultNext;
        private final int _splitIndex;
        final boolean _isLeaf;

        // set parent
        Node(ModelReader reader) throws IOException {
            parent_ = reader.readInt();
            cleft_ = reader.readInt();
            cright_ = reader.readInt();
            sindex_ = reader.readInt();

            if (isLeaf()) {
                leaf_value = reader.readFloat();
                split_cond = Float.NaN;
            } else {
                split_cond = reader.readFloat();
                leaf_value = Float.NaN;
            }

            _defaultNext = cdefault();
            _splitIndex = getSplitIndex();
            _isLeaf = isLeaf();
        }

        public boolean isLeaf() {
            return cleft_ == -1;
        }

        @Override
        public int getSplitIndex() {
            return (int) (sindex_ & ((1l << 31) - 1l));
        }

        public int cdefault() {
            return default_left() ? cleft_ : cright_;
        }

        @Override
        public boolean default_left() {
            return (sindex_ >>> 31) != 0;
        }

        @Override
        public int next(FVec feat) {
            float value = feat.fvalue(_splitIndex);
            if (value != value) {  // is NaN?
                return _defaultNext;
            }
            return (value < split_cond) ? cleft_ : cright_;
        }

        @Override
        public int getParentIndex() {
            return parent_;
        }

        @Override
        public int getLeftChildIndex() {
            return cleft_;
        }

        @Override
        public int getRightChildIndex() {
            return cright_;
        }

        @Override
        public float getSplitCondition() {
            return split_cond;
        }

        @Override
        public float getLeafValue(){
            return leaf_value;
        }
    }

    /**
     * Statistics each node in tree.
     */
    static class RTreeNodeStat implements INodeStat, Serializable {
        /*! \brief loss chg caused by current split */
        final float loss_chg;
        /*! \brief sum of hessian values, used to measure coverage of data */
        final float sum_hess;
        /*! \brief weight of current node */
        final float base_weight;
        /*! \brief number of child that is leaf node known up to now */
        final int leaf_child_cnt;

        RTreeNodeStat(ModelReader reader) throws IOException {
            loss_chg = reader.readFloat();
            sum_hess = reader.readFloat();
            base_weight = reader.readFloat();
            leaf_child_cnt = reader.readInt();
        }

        @Override
        public float getWeight() {
            return sum_hess;
        }
    }
}


/*
 *
 * Copyright 2015 Wei-Ming Wu
 *
 * 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.github.wnameless.json.unflattener;

import static com.github.wnameless.json.flattener.FlattenMode.MONGODB;
import static org.apache.commons.lang3.Validate.isTrue;
import static org.apache.commons.lang3.Validate.notNull;

import java.io.IOException;
import java.io.Reader;
import java.io.StringWriter;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import com.eclipsesource.json.Json;
import com.eclipsesource.json.JsonArray;
import com.eclipsesource.json.JsonObject;
import com.eclipsesource.json.JsonValue;
import com.eclipsesource.json.PrettyPrint;
import com.eclipsesource.json.WriterConfig;
import com.github.wnameless.json.flattener.FlattenMode;
import com.github.wnameless.json.flattener.KeyTransformer;
import com.github.wnameless.json.flattener.PrintMode;

/**
 *
 * {@link JsonUnflattener} provides a static {@link #unflatten(String)} method
 * to unflatten any flattened JSON string back to nested one.
 *
 * @author Wei-Ming Wu
 *
 */
public final class JsonUnflattener {

  /**
   * Returns a JSON string of nested objects by the given flattened JSON string.
   *
   * @param json
   *          a flattened JSON string
   * @return a JSON string of nested objects
   */
  public static String unflatten(String json) {
    return new JsonUnflattener(json).unflatten();
  }

  private final JsonValue root;

  private FlattenMode flattenMode = FlattenMode.NORMAL;
  private Character separator = '.';
  private Character leftBracket = '[';
  private Character rightBracket = ']';
  private PrintMode printMode = PrintMode.MINIMAL;
  private KeyTransformer keyTrans = null;

  /**
   * Creates a JSON unflattener.
   *
   * @param json
   *          the JSON string
   */
  public JsonUnflattener(String json) {
    root = Json.parse(json);
  }

  /**
   * Creates a JSON unflattener.
   *
   * @param jsonReader
   *          the JSON reader
   * @throws IOException
   *           if jsonReader cannot be read
   */
  public JsonUnflattener(Reader jsonReader) throws IOException {
    root = Json.parse(jsonReader);
  }

  private String arrayIndex() {
    return Pattern.quote(leftBracket.toString()) + "\\s*\\d+\\s*"
        + Pattern.quote(rightBracket.toString());
  }

  private String objectComplexKey() {
    return Pattern.quote(leftBracket.toString()) + "\\s*\".+?\"\\s*"
        + Pattern.quote(rightBracket.toString());
  }

  private String objectKey() {
    return "[^" + Pattern.quote(separator.toString())
        + Pattern.quote(leftBracket.toString())
        + Pattern.quote(rightBracket.toString()) + "]+";
  }

  private Pattern keyPartPattern() {
    if (flattenMode.equals(MONGODB))
      return Pattern.compile("[^" + Pattern.quote(separator.toString()) + "]+");
    else
      return Pattern
          .compile(arrayIndex() + "|" + objectComplexKey() + "|" + objectKey());
  }

  /**
   * A fluent setter to setup a mode of the {@link JsonUnflattener}.
   *
   * @param flattenMode
   *          a {@link FlattenMode}
   * @return this {@link JsonUnflattener}
   */
  public JsonUnflattener withFlattenMode(FlattenMode flattenMode) {
    this.flattenMode = notNull(flattenMode);
    return this;
  }

  /**
   * A fluent setter to setup the separator within a key in the flattened JSON.
   * The default separator is a dot(.).
   *
   * @param separator
   *          any character
   * @return this {@link JsonUnflattener}
   */
  public JsonUnflattener withSeparator(char separator) {
    isTrue(!Character.toString(separator).matches("[\"\\s]"),
        "Separator contains illegal chracter(%s)",
        Character.toString(separator));
    isTrue(!leftBracket.equals(separator) && !rightBracket.equals(separator),
        "Separator(%s) is already used in brackets",
        Character.toString(separator));

    this.separator = separator;
    return this;
  }

  private String illegalBracketsRegex() {
    return "[\"\\s" + Pattern.quote(separator.toString()) + "]";
  }

  /**
   * A fluent setter to setup the left and right brackets within a key in the
   * flattened JSON. The default left and right brackets are left square
   * bracket([) and right square bracket(]).
   *
   * @param leftBracket
   *          any character
   * @param rightBracket
   *          any character
   * @return this {@link JsonUnflattener}
   */
  public JsonUnflattener withLeftAndRightBrackets(char leftBracket,
      char rightBracket) {
    isTrue(leftBracket != rightBracket, "Both brackets cannot be the same");
    isTrue(!Character.toString(leftBracket).matches(illegalBracketsRegex()),
        "Left bracket contains illegal chracter(%s)",
        Character.toString(leftBracket));
    isTrue(!Character.toString(rightBracket).matches(illegalBracketsRegex()),
        "Right bracket contains illegal chracter(%s)",
        Character.toString(rightBracket));

    this.leftBracket = leftBracket;
    this.rightBracket = rightBracket;
    return this;
  }

  /**
   * A fluent setter to setup a print mode of the {@link JsonUnflattener}. The
   * default print mode is minimal.
   *
   * @param printMode
   *          a {@link PrintMode}
   * @return this {@link JsonUnflattener}
   */
  public JsonUnflattener withPrintMode(PrintMode printMode) {
    this.printMode = notNull(printMode);
    return this;
  }

  /**
   * A fluent setter to setup a {@link KeyTransformer} of the
   * {@link JsonUnflattener}.
   *
   * @param keyTrans
   *          a {@link KeyTransformer}
   * @return this {@link JsonUnflattener}
   */
  public JsonUnflattener withKeyTransformer(KeyTransformer keyTrans) {
    this.keyTrans = notNull(keyTrans);
    return this;
  }

  private WriterConfig getWriterConfig() {
    switch (printMode) {
      case REGULAR:
        return PrettyPrint.singleLine();
      case PRETTY:
        return WriterConfig.PRETTY_PRINT;
      default:
        return WriterConfig.MINIMAL;
    }
  }

  /**
   * Returns a JSON string of nested objects by the given flattened JSON string.
   *
   * @return a JSON string of nested objects
   */
  public String unflatten() {
    StringWriter sw = new StringWriter();
    if (root.isArray()) {
      try {
        unflattenArray(root.asArray()).writeTo(sw, getWriterConfig());
      } catch (IOException e) {}
      return sw.toString();
    }
    if (!root.isObject()) {
      return root.toString();
    }

    JsonObject flattened = root.asObject();
    JsonValue unflattened = flattened.names().isEmpty() ? Json.object() : null;

    for (String key : flattened.names()) {
      JsonValue currentVal = unflattened;
      String objKey = null;
      Integer aryIdx = null;

      Matcher matcher = keyPartPattern().matcher(key);
      while (matcher.find()) {
        String keyPart = matcher.group();

        if (objKey != null ^ aryIdx != null) {
          if (isJsonArray(keyPart)) {
            currentVal = findOrCreateJsonArray(currentVal, objKey, aryIdx);
            objKey = null;
            aryIdx = extractIndex(keyPart);
          } else { // JSON object
            if (flattened.get(key).isArray()) { // KEEP_ARRAYS mode
              flattened.set(key, unflattenArray(flattened.get(key).asArray()));
            }
            currentVal = findOrCreateJsonObject(currentVal, objKey, aryIdx);
            objKey = extractKey(keyPart);
            aryIdx = null;
          }
        }

        if (objKey == null && aryIdx == null) {
          if (isJsonArray(keyPart)) {
            aryIdx = extractIndex(keyPart);
            if (currentVal == null) currentVal = Json.array();
          } else { // JSON object
            objKey = extractKey(keyPart);
            if (currentVal == null) currentVal = Json.object();
          }
        }

        if (unflattened == null) unflattened = currentVal;
      }

      setUnflattenedValue(flattened, key, currentVal, objKey, aryIdx);
    }

    try {
      unflattened.writeTo(sw, getWriterConfig());
    } catch (IOException e) {}
    return sw.toString();
  }

  private JsonArray unflattenArray(JsonArray array) {
    JsonArray unflattenArray = Json.array().asArray();

    for (JsonValue value : array) {
      if (value.isArray()) {
        unflattenArray.add(unflattenArray(value.asArray()));
      } else if (value.isObject()) {
        unflattenArray.add(Json.parse(new JsonUnflattener(value.toString())
            .withSeparator(separator).unflatten()));
      } else {
        unflattenArray.add(value);
      }
    }

    return unflattenArray;
  }

  private String extractKey(String keyPart) {
    if (keyPart.matches(objectComplexKey())) {
      keyPart = keyPart.replaceAll(
          "^" + Pattern.quote(leftBracket.toString()) + "\\s*\"", "");
      keyPart = keyPart.replaceAll(
          "\"\\s*" + Pattern.quote(rightBracket.toString()) + "$", "");
    }
    return keyTrans != null ? keyTrans.transform(keyPart) : keyPart;
  }

  private Integer extractIndex(String keyPart) {
    if (flattenMode.equals(MONGODB))
      return Integer.valueOf(keyPart);
    else
      return Integer.valueOf(
          keyPart.replaceAll("[" + Pattern.quote(leftBracket.toString())
              + Pattern.quote(rightBracket.toString()) + "\\s]", ""));
  }

  private boolean isJsonArray(String keyPart) {
    return keyPart.matches(arrayIndex())
        || (flattenMode.equals(MONGODB) && keyPart.matches("\\d+"));
  }

  private JsonValue findOrCreateJsonArray(JsonValue currentVal, String objKey,
      Integer aryIdx) {
    if (objKey != null) {
      JsonObject jsonObj = currentVal.asObject();

      if (jsonObj.get(objKey) == null) {
        JsonValue ary = Json.array();
        jsonObj.add(objKey, ary);

        return ary;
      }

      return jsonObj.get(objKey);
    } else { // aryIdx != null
      JsonArray jsonAry = currentVal.asArray();

      if (jsonAry.size() <= aryIdx || jsonAry.get(aryIdx).equals(Json.NULL)) {
        JsonValue ary = Json.array();
        assureJsonArraySize(jsonAry, aryIdx);
        jsonAry.set(aryIdx, ary);

        return ary;
      }

      return jsonAry.get(aryIdx);
    }
  }

  private JsonValue findOrCreateJsonObject(JsonValue currentVal, String objKey,
      Integer aryIdx) {
    if (objKey != null) {
      JsonObject jsonObj = currentVal.asObject();

      if (jsonObj.get(objKey) == null) {
        JsonValue obj = Json.object();
        jsonObj.add(objKey, obj);

        return obj;
      }

      return jsonObj.get(objKey);
    } else { // aryIdx != null
      JsonArray jsonAry = currentVal.asArray();

      if (jsonAry.size() <= aryIdx || jsonAry.get(aryIdx).equals(Json.NULL)) {
        JsonValue obj = Json.object();
        assureJsonArraySize(jsonAry, aryIdx);
        jsonAry.set(aryIdx, obj);

        return obj;
      }

      return jsonAry.get(aryIdx);
    }
  }

  private void setUnflattenedValue(JsonObject flattened, String key,
      JsonValue currentVal, String objKey, Integer aryIdx) {
    JsonValue val = flattened.get(key);
    if (objKey != null) {
      if (val.isArray()) {
        JsonValue jsonArray = Json.array();
        for (JsonValue arrayVal : val.asArray()) {
          jsonArray.asArray().add(
              Json.parse(newJsonUnflattener(arrayVal.toString()).unflatten()));
        }
        currentVal.asObject().add(objKey, jsonArray);
      } else {
        currentVal.asObject().add(objKey, val);
      }
    } else { // aryIdx != null
      assureJsonArraySize(currentVal.asArray(), aryIdx);
      currentVal.asArray().set(aryIdx, val);
    }
  }

  private JsonUnflattener newJsonUnflattener(String json) {
    JsonUnflattener jf = new JsonUnflattener(json);
    if (flattenMode != null) jf.withFlattenMode(flattenMode);
    if (keyTrans != null) jf.withKeyTransformer(keyTrans);
    if (leftBracket != null && rightBracket != null)
      jf.withLeftAndRightBrackets(leftBracket, rightBracket);
    if (separator != null) jf.withSeparator(separator);
    return jf;
  }

  private void assureJsonArraySize(JsonArray jsonArray, Integer index) {
    while (index >= jsonArray.size()) {
      jsonArray.add(Json.NULL);
    }
  }

  @Override
  public int hashCode() {
    int result = 27;
    result = 31 * result + root.hashCode();
    return result;
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (!(o instanceof JsonUnflattener)) return false;
    return root.equals(((JsonUnflattener) o).root);
  }

  @Override
  public String toString() {
    return "JsonUnflattener{root=" + root + "}";
  }

}


/*
 *
 * Copyright 2015 Wei-Ming Wu
 *
 * 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.github.wnameless.json.flattener;

import java.util.Iterator;
import java.util.NoSuchElementException;

/**
 *
 * {@link IndexedPeekIterator} is an Iterator which provides user a
 * {@link #peek()} method to peek an element advanced, a {@link #getIndex()}
 * method to get the index of last returned element and a {@link #getCurrent()}
 * method to get the last returned element itself.
 *
 * @param <E>
 *          the type of elements
 *
 * @author Wei-Ming Wu
 *
 */
public final class IndexedPeekIterator<E> implements Iterator<E> {

  /**
   * Creates an {@link IndexedPeekIterator} by given Iterable.
   *
   * @param <T>
   *          the type of elements
   * @param iter
   *          any Iterable
   * @return an {@link IndexedPeekIterator}
   */
  public static <T> IndexedPeekIterator<T> newIndexedPeekIterator(
      Iterable<T> iter) {
    return new IndexedPeekIterator<T>(iter.iterator());
  }

  private final Iterator<? extends E> iterator;
  private E peek;
  private boolean hasPeek = false;
  private int index = -1;
  private E current = null;

  /**
   * Creates an {@link IndexedPeekIterator}.
   *
   * @param iterator
   *          an Iterator
   */
  public IndexedPeekIterator(Iterator<? extends E> iterator) {
    if (iterator == null) throw new NullPointerException();

    this.iterator = iterator;
  }

  private void peeking() {
    peek = iterator.next();
    hasPeek = true;
  }

  /**
   * Returns the index of last returned element. If there is no element has been
   * returned, it returns -1.
   *
   * @return the index of last returned element
   */
  public int getIndex() {
    return index;
  }

  /**
   * Returns the last returned element. If {@link #next()} has never been
   * called, it returns null.
   *
   * @return the last returned element
   */
  public E getCurrent() {
    return current;
  }

  @Override
  public boolean hasNext() {
    return hasPeek || iterator.hasNext();
  }

  @Override
  public E next() {
    if (!hasNext()) throw new NoSuchElementException();

    index++;
    if (hasPeek) {
      hasPeek = false;
      return current = peek;
    } else {
      peeking();
      return next();
    }
  }

  @Override
  public void remove() {
    if (hasPeek) throw new IllegalStateException();

    iterator.remove();
  }

  /**
   * Peeks an element advanced. Warning: remove() is temporarily out of function
   * after a peek() until a next() is called.
   *
   * @return element
   */
  public E peek() {
    if (!hasPeek && hasNext()) peeking();
    if (!hasPeek) throw new NoSuchElementException();

    return peek;
  }

}


/*
 *
 * Copyright 2015 Wei-Ming Wu
 *
 * 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.github.wnameless.json.flattener;

import static com.github.wnameless.json.flattener.FlattenMode.MONGODB;
import static com.github.wnameless.json.flattener.IndexedPeekIterator.newIndexedPeekIterator;
import static java.util.Collections.emptyMap;
import static org.apache.commons.lang3.Validate.isTrue;
import static org.apache.commons.lang3.Validate.notNull;

import java.io.IOException;
import java.io.Reader;
import java.math.BigDecimal;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Map;
import java.util.regex.Pattern;

import org.apache.commons.lang3.StringUtils;

import com.eclipsesource.json.Json;
import com.eclipsesource.json.JsonObject.Member;
import com.eclipsesource.json.JsonValue;

/**
 *
 * {@link JsonFlattener} flattens any JSON nested objects or arrays into a
 * flattened JSON string or a Map{@literal <Stirng, Object>}. The String key
 * will represents the corresponding position of value in the original nested
 * objects or arrays and the Object value are either String, Boolean, Long,
 * Double or null.

 *

 * For example:

 * A nested JSON

 * { "a" : { "b" : 1, "c": null, "d": [false, true] }, "e": "f", "g":2.3 }

 *

 * can be turned into a flattened JSON

 * { "a.b": 1, "a.c": null, "a.d[0]": false, "a.d[1]": true, "e": "f", "g":2.3 }
 *

 *

 * or into a Map

 * {

 *   a.b=1,

 *   a.c=null,

 *   a.d[0]=false,

 *   a.d[1]=true,

 *   e=f,

 *   g=2.3

 * }
 *
 * @author Wei-Ming Wu
 *
 */
public final class JsonFlattener {

  /**
   * {@link ROOT} is the default key of the Map returned by
   * {@link #flattenAsMap}. When {@link JsonFlattener} processes a JSON string
   * which is not a JSON object or array, the final outcome may not suit in a
   * Java Map. At that moment, {@link JsonFlattener} will put the result in the
   * Map with {@link ROOT} as its key.
   */
  public static final String ROOT = "root";

  /**
   * Returns a flattened JSON string.
   *
   * @param json
   *          the JSON string
   * @return a flattened JSON string
   */
  public static String flatten(String json) {
    return new JsonFlattener(json).flatten();
  }

  /**
   * Returns a flattened JSON as Map.
   *
   * @param json
   *          the JSON string
   * @return a flattened JSON as Map
   */
  public static Map<String, Object> flattenAsMap(String json) {
    return new JsonFlattener(json).flattenAsMap();
  }

  private final JsonValue source;

  private JsonifyLinkedHashMap<String, Object> flattenedMap;
  private final Deque<IndexedPeekIterator<?>> elementIters = new ArrayDeque<>();

  private FlattenMode flattenMode = FlattenMode.NORMAL;
  private CharSequenceTranslatorFactory policy = StringEscapePolicy.DEFAULT;
  private Character separator = '.';
  private Character leftBracket = '[';
  private Character rightBracket = ']';
  private PrintMode printMode = PrintMode.MINIMAL;
  private KeyTransformer keyTrans = null;

  /**
   * Creates a JSON flattener.
   *
   * @param json
   *          the JSON string
   */
  public JsonFlattener(String json) {
    source = Json.parse(json);
  }

  /**
   * Creates a JSON flattener.
   *
   * @param jsonReader
   *          the JSON reader
   * @throws IOException
   *           if jsonReader cannot be read
   */
  public JsonFlattener(Reader jsonReader) throws IOException {
    source = Json.parse(jsonReader);
  }

  /**
   * A fluent setter to setup a mode of the {@link JsonFlattener}.
   *
   * @param flattenMode
   *          a {@link FlattenMode}
   * @return this {@link JsonFlattener}
   */
  public JsonFlattener withFlattenMode(FlattenMode flattenMode) {
    this.flattenMode = notNull(flattenMode);
    flattenedMap = null;
    return this;
  }

  /**
   * A fluent setter to setup the JSON string escape policy.
   *
   * @param policy
   *          any {@link CharSequenceTranslatorFactory} or a
   *          {@link StringEscapePolicy}
   * @return this {@link JsonFlattener}
   */
  public JsonFlattener withStringEscapePolicy(
      CharSequenceTranslatorFactory policy) {
    this.policy = notNull(policy);
    flattenedMap = null;
    return this;
  }

  /**
   * A fluent setter to setup the separator within a key in the flattened JSON.
   * The default separator is a dot(.).
   *
   * @param separator
   *          any character
   * @return this {@link JsonFlattener}
   */
  public JsonFlattener withSeparator(char separator) {
    isTrue(!Character.toString(separator).matches("[\"\\s]"),
        "Separator contains illegal chracter(%s)",
        Character.toString(separator));
    isTrue(!leftBracket.equals(separator) && !rightBracket.equals(separator),
        "Separator(%s) is already used in brackets",
        Character.toString(separator));

    this.separator = separator;
    flattenedMap = null;
    return this;
  }

  private String illegalBracketsRegex() {
    return "[\"\\s" + Pattern.quote(this.separator.toString()) + "]";
  }

  /**
   * A fluent setter to setup the left and right brackets within a key in the
   * flattened JSON. The default left and right brackets are left square
   * bracket([) and right square bracket(]).
   *
   * @param leftBracket
   *          any character
   * @param rightBracket
   *          any character
   * @return this {@link JsonFlattener}
   */
  public JsonFlattener withLeftAndRightBrackets(char leftBracket,
      char rightBracket) {
    isTrue(leftBracket != rightBracket, "Both brackets cannot be the same");
    isTrue(!Character.toString(leftBracket).matches(illegalBracketsRegex()),
        "Left bracket contains illegal chracter(%s)",
        Character.toString(leftBracket));
    isTrue(!Character.toString(rightBracket).matches(illegalBracketsRegex()),
        "Right bracket contains illegal chracter(%s)",
        Character.toString(rightBracket));

    this.leftBracket = leftBracket;
    this.rightBracket = rightBracket;
    flattenedMap = null;
    return this;
  }

  /**
   * A fluent setter to setup a print mode of the {@link JsonFlattener}. The
   * default print mode is minimal.
   *
   * @param printMode
   *          a {@link PrintMode}
   * @return this {@link JsonFlattener}
   */
  public JsonFlattener withPrintMode(PrintMode printMode) {
    this.printMode = notNull(printMode);
    return this;
  }

  /**
   * A fluent setter to setup a {@link KeyTransformer} of the
   * {@link JsonFlattener}.
   *
   * @param keyTrans
   *          a {@link KeyTransformer}
   * @return this {@link JsonFlattener}
   */
  public JsonFlattener withKeyTransformer(KeyTransformer keyTrans) {
    this.keyTrans = notNull(keyTrans);
    flattenedMap = null;
    return this;
  }

  /**
   * Returns a flattened JSON string.
   *
   * @return a flattened JSON string
   */
  public String flatten() {
    flattenAsMap();

    if (source.isObject() || isObjectifiableArray())
      return flattenedMap.toString(printMode);
    else
      return javaObj2Json(flattenedMap.get(ROOT));
  }

  private boolean isObjectifiableArray() {
    return source.isArray() && !flattenedMap.containsKey(ROOT);
  }

  private String javaObj2Json(Object obj) {
    if (obj == null) {
      return "null";
    } else if (obj instanceof CharSequence) {
      StringBuilder sb = new StringBuilder();
      sb.append('"');
      sb.append(
          policy.getCharSequenceTranslator().translate((CharSequence) obj));
      sb.append('"');
      return sb.toString();
    } else if (obj instanceof JsonifyArrayList) {
      JsonifyArrayList<?> list = (JsonifyArrayList<?>) obj;
      return list.toString(printMode);
    } else {
      return obj.toString();
    }
  }

  /**
   * Returns a flattened JSON as Map.
   *
   * @return a flattened JSON as Map
   */
  public Map<String, Object> flattenAsMap() {
    if (flattenedMap != null) return flattenedMap;

    flattenedMap = newJsonifyLinkedHashMap();
    reduce(source);

    while (!elementIters.isEmpty()) {
      IndexedPeekIterator<?> deepestIter = elementIters.getLast();
      if (!deepestIter.hasNext()) {
        elementIters.removeLast();
      } else if (deepestIter.peek() instanceof Member) {
        Member mem = (Member) deepestIter.next();
        reduce(mem.getValue());
      } else { // JsonValue
        JsonValue val = (JsonValue) deepestIter.next();
        reduce(val);
      }
    }

    return flattenedMap;
  }

  private void reduce(JsonValue val) {
    if (val.isObject() && val.asObject().iterator().hasNext()) {
      elementIters.add(newIndexedPeekIterator(val.asObject()));
    } else if (val.isArray() && val.asArray().iterator().hasNext()) {
      switch (flattenMode) {
        case KEEP_ARRAYS:
          JsonifyArrayList<Object> array = newJsonifyArrayList();
          for (JsonValue value : val.asArray()) {
            array.add(jsonVal2Obj(value));
          }
          flattenedMap.put(computeKey(), array);
          break;
        default:
          elementIters.add(newIndexedPeekIterator(val.asArray()));
      }
    } else {
      String key = computeKey();
      Object value = jsonVal2Obj(val);
      // Check NOT empty JSON object
      if (!(ROOT.equals(key) && emptyMap().equals(value)))
        flattenedMap.put(key, jsonVal2Obj(val));
    }
  }

  private Object jsonVal2Obj(JsonValue val) {
    if (val.isBoolean()) return val.asBoolean();
    if (val.isString()) return val.asString();
    if (val.isNumber()) return new BigDecimal(val.toString());
    switch (flattenMode) {
      case KEEP_ARRAYS:
        if (val.isArray()) {
          JsonifyArrayList<Object> array = newJsonifyArrayList();
          for (JsonValue value : val.asArray()) {
            array.add(jsonVal2Obj(value));
          }
          return array;
        } else if (val.isObject()) {
          if (val.asObject().iterator().hasNext()) {
            return newJsonFlattener(val.toString()).flattenAsMap();
          } else {
            return newJsonifyLinkedHashMap();
          }
        }
      default:
        if (val.isArray()) {
          return newJsonifyArrayList();
        } else if (val.isObject()) {
          return newJsonifyLinkedHashMap();
        }
    }

    return null;
  }

  private boolean hasReservedCharacters(String key) {
    if (flattenMode.equals(MONGODB) && StringUtils.containsAny(key, separator))
      throw new IllegalArgumentException("Key cannot contain separator("
          + separator + ") in FlattenMode." + MONGODB);

    return StringUtils.containsAny(key, separator, leftBracket, rightBracket);
  }

  private String computeKey() {
    if (elementIters.isEmpty()) return ROOT;

    StringBuilder sb = new StringBuilder();

    for (IndexedPeekIterator<?> iter : elementIters) {
      if (iter.getCurrent() instanceof Member) {
        String key = ((Member) iter.getCurrent()).getName();
        if (keyTrans != null) key = keyTrans.transform(key);
        if (hasReservedCharacters(key)) {
          sb.append(leftBracket);
          sb.append('\\');
          sb.append('"');
          sb.append(policy.getCharSequenceTranslator().translate(key));
          sb.append('\\');
          sb.append('"');
          sb.append(rightBracket);
        } else {
          if (sb.length() != 0) sb.append(separator);
          sb.append(policy.getCharSequenceTranslator().translate(key));
        }
      } else { // JsonValue
        sb.append(flattenMode.equals(MONGODB) ? separator : leftBracket);
        sb.append(iter.getIndex());
        sb.append(flattenMode.equals(MONGODB) ? "" : rightBracket);
      }
    }

    return sb.toString();
  }

  private <T> JsonifyArrayList<T> newJsonifyArrayList() {
    JsonifyArrayList<T> array = new JsonifyArrayList<>();
    array.setTranslator(policy.getCharSequenceTranslator());
    return array;
  }

  private <K, V> JsonifyLinkedHashMap<K, V> newJsonifyLinkedHashMap() {
    JsonifyLinkedHashMap<K, V> map = new JsonifyLinkedHashMap<>();
    map.setTranslator(policy.getCharSequenceTranslator());
    return map;
  }

  private JsonFlattener newJsonFlattener(String json) {
    return new JsonFlattener(json).withFlattenMode(flattenMode)
        .withSeparator(separator).withStringEscapePolicy(policy)
        .withPrintMode(printMode);
  }

  @Override
  public int hashCode() {
    int result = 27;
    result = 31 * result + source.hashCode();
    return result;
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (!(o instanceof JsonFlattener)) return false;
    return source.equals(((JsonFlattener) o).source);
  }

  @Override
  public String toString() {
    return "JsonFlattener{source=" + source + "}";
  }

}


package org.reactfx.util;

import javafx.animation.Animation;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.util.Duration;

/**
 * Provides factory methods for timers that are manipulated from and execute
 * their action on the JavaFX application thread.
 */
public class FxTimer implements Timer {

    /**
     * Prepares a (stopped) timer that lasts for {@code delay} and whose action runs when timer ends.
     */
    public static Timer create(java.time.Duration delay, Runnable action) {
        return new FxTimer(delay, delay, action, 1);
    }

    /**
     * Equivalent to {@code create(delay, action).restart()}.
     */
    public static Timer runLater(java.time.Duration delay, Runnable action) {
        Timer timer = create(delay, action);
        timer.restart();
        return timer;
    }

    /**
     * Prepares a (stopped) timer that lasts for {@code interval} and that executes the given action periodically
     * when the timer ends.
     */
    public static Timer createPeriodic(java.time.Duration interval, Runnable action) {
        return new FxTimer(interval, interval, action, Animation.INDEFINITE);
    }

    /**
     * Equivalent to {@code createPeriodic(interval, action).restart()}.
     */
    public static Timer runPeriodically(java.time.Duration interval, Runnable action) {
        Timer timer = createPeriodic(interval, action);
        timer.restart();
        return timer;
    }

    /**
     * Prepares a (stopped) timer that lasts for {@code interval} and that executes the given action periodically
     * when the timer starts.
     */
    public static Timer createPeriodic0(java.time.Duration interval, Runnable action) {
        return new FxTimer(java.time.Duration.ZERO, interval, action, Animation.INDEFINITE);
    }

    /**
     * Equivalent to {@code createPeriodic0(interval, action).restart()}.
     */
    public static Timer runPeriodically0(java.time.Duration interval, Runnable action) {
        Timer timer = createPeriodic0(interval, action);
        timer.restart();
        return timer;
    }

    private final Duration actionTime;
    private final Timeline timeline;
    private final Runnable action;

    private long seq = 0;

    private FxTimer(java.time.Duration actionTime, java.time.Duration period, Runnable action, int cycles) {
        this.actionTime = Duration.millis(actionTime.toMillis());
        this.timeline = new Timeline();
        this.action = action;

        timeline.getKeyFrames().add(new KeyFrame(this.actionTime)); // used as placeholder
        if (period != actionTime) {
            timeline.getKeyFrames().add(new KeyFrame(Duration.millis(period.toMillis())));
        }

        timeline.setCycleCount(cycles);
    }

    @Override
    public void restart() {
        stop();
        long expected = seq;
        timeline.getKeyFrames().set(0, new KeyFrame(actionTime, ae -> {
            if(seq == expected) {
                action.run();
            }
        }));
        timeline.play();
    }

    @Override
    public void stop() {
        timeline.stop();
        ++seq;
    }
}


package org.reactfx;

import java.util.function.Consumer;

import org.reactfx.util.ListHelper;
import org.reactfx.util.NotificationAccumulator;

/**
 * Base class for observable objects. This abstract class implements:
 *

 *
Observer management: adding and removing observers.

 *
Lazy binding to inputs. An observable has 0 or more inputs,
 *   most commonly, but not necessarily, other observables. Lazy binding to
 *   inputs means that the observable observes its inputs only when it is
 *   itself being observed.

 *
Observer notification.

 *

 *
 * @param <O> type of the observer
 * @param <T> type of produced values
 */
public abstract class ObservableBase<O, T> implements ProperObservable<O, T> {
    private ListHelper<O> observers = null;
    private Subscription inputSubscription = null;
    private final NotificationAccumulator<O, T, ?> pendingNotifications;

    protected ObservableBase(NotificationAccumulator<O, T, ?> pendingNotificationsImpl) {
        this.pendingNotifications = pendingNotificationsImpl;
    }

    protected ObservableBase() {
        this.pendingNotifications = defaultNotificationAccumulator();
    }

    /**
     * Starts observing this observable's input(s), if any.
     * This method is called when the number of observers goes from 0 to 1.
     * This method is called before {@link #newObserver(Object)}
     * is called for the first observer.
     * @return subscription used to stop observing inputs. The subscription
     * is unsubscribed (i.e. input observation stops) when the number of
     * observers goes down to 0.
     */
    protected abstract Subscription observeInputs();

    public final boolean isObservingInputs() {
        return inputSubscription != null;
    }

    protected final int getObserverCount() {
        return ListHelper.size(observers);
    }

    @Override
    public final void notifyObservers(T event) {
        enqueueNotifications(event);
        notifyObservers();
    }

    protected final void enqueueNotifications(T event) {
        // may throw if pendingNotifications not empty and recursion not allowed
        pendingNotifications.addAll(ListHelper.iterator(observers), event);
    }

    protected final void notifyObservers() {
        try {
            while(!pendingNotifications.isEmpty()) {
                pendingNotifications.takeOne().run(); // run() may throw
            }
        } finally {
            pendingNotifications.clear();
        }
    }

    /**
     * Executes action for each observer, regardless of recursion state.
     * If {@code action} throws an exception for one observer, it will not
     * be called for any subsequent observers and the exception will be
     * propagated by this method.
     * @param action action to execute for each observer.
     */
    protected final void forEachObserver(Consumer<O> action) {
        ListHelper.forEach(observers, o -> action.accept(o));
    }

    /**
     * Called for each new observer.
     * Overriding this method is a convenient way for subclasses
     * to handle this event, for example to publish some initial events.
     *
     *
This method is called after the
     * {@link #observeInputs()} method.

     */
    protected void newObserver(O observer) {
        // default implementation is empty
    }

    @Override
    public final Subscription observe(O observer) {
        addObserver(observer);
        return () -> removeObserver(observer);
    }

    @Override
    public final void addObserver(O observer) {
        observers = ListHelper.add(observers, observer);
        if(ListHelper.size(observers) == 1) {
            inputSubscription = observeInputs();
        }
        newObserver(observer);
    }

    @Override
    public final void removeObserver(O observer) {
        observers = ListHelper.remove(observers, observer);
        if(ListHelper.isEmpty(observers) && inputSubscription != null) {
            inputSubscription.unsubscribe();
            inputSubscription = null;
        }
    }

    @Override
    public int hashCode() {
        return defaultHashCode();
    }

    @Override
    public boolean equals(Object o) {
        return defaultEquals(o);
    }

    @Override
    public String toString() {
        return defaultToString();
    }
}


package org.reactfx.util;

import static org.reactfx.util.Tuples.*;

import java.util.AbstractList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Objects;
import java.util.function.Function;

public final class Lists {

    // private constructor to prevent instantiation
    private Lists() {}

    public static <E> int hashCode(List<E> list) {
        int hashCode = 1;
        for(E e: list) {
            hashCode = 31 * hashCode + Objects.hashCode(e);
        }
        return hashCode;
    }

    public static boolean equals(List<?> list, Object o) {
        if(o == list) return true;

        if(!(o instanceof List)) return false;

        List<?> that = (List<?>) o;

        if(list.size() != that.size()) return false;

        Iterator<?> it1 = list.iterator();
        Iterator<?> it2 = that.iterator();
        while(it1.hasNext()) {
            if(!Objects.equals(it1.next(), it2.next())) {
                return false;
            }
        }

        return true;
    }

    public static String toString(List<?> list) {
        StringBuilder res = new StringBuilder();
        res.append('[');
        Iterator<?> it = list.iterator();
        while(it.hasNext()) {
            res.append(it.next());
            if(it.hasNext()) {
                res.append(", ");
            }
        }
        res.append(']');
        return res.toString();
    }

    public static <E> Iterator<E> readOnlyIterator(Collection<? extends E> col) {
        return new Iterator<E>() {
            private final Iterator<? extends E> delegate = col.iterator();

            @Override
            public boolean hasNext() {
                return delegate.hasNext();
            }

            @Override
            public E next() {
                return delegate.next();
            }
        };
    }

    public static boolean isValidIndex(int index, int size) {
        return isValidIndex(0, index, size);
    }

    public static boolean isValidIndex(int min, int index, int max) {
        return min <= index && index < max;
    }

    public static void checkIndex(int index, int size) {
        checkIndex(0, index, size);
    }

    public static void checkIndex(int min, int index, int max) {
        if(!isValidIndex(min, index, max)) {
            throw new IndexOutOfBoundsException(index + " not in [" + min + ", " + max + ")");
        }
    }

    public static boolean isValidPosition(int position, int size) {
        return isValidPosition(0, position, size);
    }

    public static boolean isValidPosition(int min, int position, int max) {
        return min <= position && position <= max;
    }

    public static void checkPosition(int position, int size) {
        checkPosition(0, position, size);
    }

    public static void checkPosition(int min, int position, int max) {
        if(!isValidPosition(min, position, max)) {
            throw new IndexOutOfBoundsException(position + " not in [" + min + ", " + max + "]");
        }
    }

    public static boolean isValidRange(int from, int to, int size) {
        return isValidRange(0, from, to, size);
    }

    public static boolean isValidRange(int min, int from, int to, int max) {
        return min <= from && from <= to && to <= max;
    }

    public static void checkRange(int from, int to, int size) {
        checkRange(0, from, to, size);
    }

    public static void checkRange(int min, int from, int to, int max) {
        if(!isValidRange(min, from, to, max)) {
            throw new IndexOutOfBoundsException(
                    "[" + from + ", " + to + ") is not a valid range within " +
                    "[" + min + ", " + max + ")");
        }
    }

    public static boolean isNonEmptyRange(int from, int to, int size) {
        return isNonEmptyRange(0, from, to, size);
    }

    public static boolean isNonEmptyRange(int min, int from, int to, int max) {
        return min <= from && from < to && to <= max;
    }

    public static boolean isProperRange(int from, int to, int size) {
        return isProperRange(0, from, to, size);
    }

    public static boolean isProperRange(int min, int from, int to, int max) {
        return isValidRange(min, from, to, max) && (min < from || to < max);
    }

    public static boolean isProperNonEmptyRange(int from, int to, int size) {
        return isProperNonEmptyRange(0, from, to, size);
    }

    public static boolean isProperNonEmptyRange(int min, int from, int to, int max) {
        return isNonEmptyRange(min, from, to, max) && (min < from || to < max);
    }

    public static boolean isStrictlyInsideRange(int from, int to, int size) {
        return isStrictlyInsideRange(0, from, to, size);
    }

    public static boolean isStrictlyInsideRange(int min, int from, int to, int max) {
        return min < from && from <= to && to < max;
    }

    public static boolean isStrictlyInsideNonEmptyRange(int from, int to, int size) {
        return isStrictlyInsideNonEmptyRange(0, from, to, size);
    }

    public static boolean isStrictlyInsideNonEmptyRange(int min, int from, int to, int max) {
        return min < from && from < to && to < max;
    }

    public static <E, F> List<F> mappedView(
            List<? extends E> source,
            Function<? super E, ? extends F> f) {
        return new AbstractList<F>() {

            @Override
            public F get(int index) {
                return f.apply(source.get(index));
            }

            @Override
            public int size() {
                return source.size();
            }
        };
    }

    @SafeVarargs
    public static <E> List<E> concatView(List<? extends E>... lists) {
        return concatView(Arrays.asList(lists));
    }

    /**
     * Returns a list that is a concatenation of the given lists. The returned
     * list is a view of the underlying lists, without copying the elements.
     * The returned list is unmodifiable. Modifications to underlying lists
     * will be visible through the concatenation view.
     */
    public static <E> List<E> concatView(List<List<? extends E>> lists) {
        if(lists.isEmpty()) {
            return Collections.emptyList();
        } else {
            return ConcatView.create(lists);
        }
    }

    @SafeVarargs
    public static <E> List<E> concat(List<? extends E>... lists) {
        return concat(Arrays.asList(lists));
    }

    /**
     * Returns a list that is a concatenation of the given lists. The returned
     * list is a view of the underlying lists, without copying the elements.
     * As opposed to {@link #concatView(List)}, the underlying lists must not
     * be modified while the returned concatenation view is in use. On the other
     * hand, this method guarantees balanced nesting if some of the underlying
     * lists are already concatenations created by this method.
     */
    public static <E> List<E> concat(List<List<? extends E>> lists) {
        return ListConcatenation.create(lists);
    }

    public static int commonPrefixLength(List<?> l, List<?> m) {
        ListIterator<?> i = l.listIterator();
        ListIterator<?> j = m.listIterator();
        while(i.hasNext() && j.hasNext()) {
            if(!Objects.equals(i.next(), j.next())) {
                return i.nextIndex() - 1;
            }
        }
        return i.nextIndex();
    }

    public static int commonSuffixLength(List<?> l, List<?> m) {
        ListIterator<?> i = l.listIterator(l.size());
        ListIterator<?> j = m.listIterator(m.size());
        while(i.hasPrevious() && j.hasPrevious()) {
            if(!Objects.equals(i.previous(), j.previous())) {
                return l.size() - i.nextIndex() - 1;
            }
        }
        return l.size() - i.nextIndex();
    }

    /**
     * Returns the lengths of common prefix and common suffix of two lists.
     * The total of the two lengths returned is not greater than either of
     * the list sizes. Prefix is prioritized: for lists [a, b, a, b], [a, b]
     * returns (2, 0); although the two lists have a common suffix of length 2,
     * the length of the second list is already included in the length of the
     * common prefix.
     */
    public static Tuple2<Integer, Integer> commonPrefixSuffixLengths(List<?> l1, List<?> l2) {
        int n1 = l1.size();
        int n2 = l2.size();

        if(n1 == 0 || n2 == 0) {
            return t(0, 0);
        }

        int pref = commonPrefixLength(l1, l2);
        if(pref == n1 || pref == n2) {
            return t(pref, 0);
        }

        int suff = commonSuffixLength(l1, l2);

        return t(pref, suff);
    }
}

class ConcatView<E> extends AbstractList<E> {

    static <E> List<E> create(List<List<? extends E>> lists) {
        return concatView(lists, true);
    }

    private static <E> List<E> concatView(
            List<List<? extends E>> lists, boolean makeUnmodifiable) {
        int len = lists.size();
        if(len < 1) {
            throw new AssertionError("Supposedly unreachable code");
        } else if(len == 1) {
            List<? extends E> list = lists.get(0);
            if(makeUnmodifiable) {
                return Collections.unmodifiableList(list);
            } else {
                @SuppressWarnings("unchecked")
                List<E> lst = (List<E>) list;
                return lst;
            }
        } else {
            int mid = len / 2;
            return new ConcatView<>(
                    concatView(lists.subList(0, mid), false),
                    concatView(lists.subList(mid, len), false));
        }
    }

    private final List<? extends E> first;
    private final List<? extends E> second;

    ConcatView(List<? extends E> first, List<? extends E> second) {
        this.first = first;
        this.second = second;
    }

    @Override
    public E get(int index) {
        if(index < first.size()) {
            return first.get(index);
        } else {
            return second.get(index - first.size());
        }
    }

    @Override
    public int size() {
        return first.size() + second.size();
    }
}

class ListConcatenation<E> extends AbstractList<E> {

    private static final ToSemigroup<List<?>, Integer> LIST_SIZE_MONOID =
            new ToSemigroup<List<?>, Integer>() {

                @Override
                public Integer apply(List<?> t) {
                    return t.size();
                }

                @Override
                public Integer reduce(Integer left, Integer right) {
                    return left + right;
                }

            };

    static <E> List<E> create(List<List<? extends E>> lists) {
        return lists.stream()
            .filter(l -> !l.isEmpty())
            .map(l -> {
                @SuppressWarnings("unchecked") // cast safe because l is unmodifiable
                List<E> lst = (List<E>) l;
                return lst instanceof ListConcatenation
                    ? ((ListConcatenation<E>) lst).ft
                    : FingerTree.mkTree(Collections.singletonList(lst), LIST_SIZE_MONOID);
            })
            .reduce(FingerTree::join)
            .<List<E>>map(ListConcatenation<E>::new)
            .orElse(Collections.emptyList());
    }

    private final FingerTree<List<E>, Integer> ft;

    ListConcatenation(FingerTree<List<E>, Integer> ft) {
        this.ft = ft;
    }

    @Override
    public E get(int index) {
        return ft.get(Integer::intValue, index, List::get);
    }

    @Override
    public int size() {
        return ft.getSummary(0);
    }

    @Override
    public List<E> subList(int from, int to) {
        Lists.checkRange(from, to, size());
        return trim(to).drop(from);
    }

    private ListConcatenation<E> trim(int limit) {
        return ft.caseEmpty().<ListConcatenation<E>>unify(
                emptyTree -> this,
                neTree -> neTree.split(Integer::intValue, limit).map((l, m, r) -> {
                    FingerTree<List<E>, Integer> t =
                            m.map((lst, i) -> i == 0 ? l : l.append(lst.subList(0, i)));
                    return new ListConcatenation<>(t);
                }));
    }

    private ListConcatenation<E> drop(int n) {
        return ft.caseEmpty().<ListConcatenation<E>>unify(
                emptyTree -> this,
                neTree -> neTree.split(Integer::intValue, n).map((l, m, r) -> {
                    FingerTree<List<E>, Integer> t =
                            m.map((lst, i) -> i == lst.size() ? r : r.prepend(lst.subList(i, lst.size())));
                    return new ListConcatenation<>(t);
                }));
    }
}


package org.reactfx;

import static org.reactfx.util.Tuples.*;

import java.time.Duration;
import java.util.Collection;
import java.util.concurrent.Executor;
import java.util.concurrent.ScheduledExecutorService;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;

import javafx.animation.AnimationTimer;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.ListChangeListener;
import javafx.collections.MapChangeListener;
import javafx.collections.ObservableList;
import javafx.collections.ObservableMap;
import javafx.collections.ObservableSet;
import javafx.collections.SetChangeListener;
import javafx.event.Event;
import javafx.event.EventHandler;
import javafx.event.EventType;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.MenuItem;
import javafx.stage.Window;

import org.reactfx.collection.ListModification;
import org.reactfx.collection.LiveList;
import org.reactfx.util.Either;
import org.reactfx.util.FxTimer;
import org.reactfx.util.Timer;
import org.reactfx.util.Tuple2;
import org.reactfx.util.Tuple3;
import org.reactfx.util.Tuple4;
import org.reactfx.util.Tuple5;
import org.reactfx.util.Tuple6;

public class EventStreams {

    private static final class Never<T>
    extends RigidObservable<Consumer<? super T>>
    implements EventStream<T> {}

    private static final EventStream<?> NEVER = new Never<Object>();

    /**
     * Returns an event stream that never emits any value.
     */
    @SuppressWarnings("unchecked")
    public static <T> EventStream<T> never() {
        return (EventStream<T>) NEVER;
    }

    /**
     * Creates an event stream that emits an impulse on every invalidation
     * of the given observable.
     */
    public static EventStream<Void> invalidationsOf(Observable observable) {
        return new EventStreamBase<Void>() {
            @Override
            protected Subscription observeInputs() {
                InvalidationListener listener = obs -> emit(null);
                observable.addListener(listener);
                return () -> observable.removeListener(listener);
            }
        };
    }

    /**
     * Creates an event stream that emits the given observable immediately for
     * every subscriber and re-emits it on every subsequent invalidation of the
     * observable.
     */
    public static <O extends Observable>
    EventStream<O> repeatOnInvalidation(O observable) {
        return new EventStreamBase<O>() {
            @Override
            protected Subscription observeInputs() {
                InvalidationListener listener = obs -> emit(observable);
                observable.addListener(listener);
                return () -> observable.removeListener(listener);
            }

            @Override
            protected void newObserver(Consumer<? super O> subscriber) {
                subscriber.accept(observable);
            }
        };
    }

    /**
     * Creates an event stream that emits the value of the given
     * {@code ObservableValue} immediately for every subscriber and then on
     * every change.
     */
    public static <T> EventStream<T> valuesOf(ObservableValue<T> observable) {
        return new EventStreamBase<T>() {
            @Override
            protected Subscription observeInputs() {
                ChangeListener<T> listener = (obs, old, val) -> emit(val);
                observable.addListener(listener);
                return () -> observable.removeListener(listener);
            }

            @Override
            protected void newObserver(Consumer<? super T> subscriber) {
                subscriber.accept(observable.getValue());
            }
        };
    }

    public static <T> EventStream<T> nonNullValuesOf(ObservableValue<T> observable) {
        return new EventStreamBase<T>() {
            @Override
            protected Subscription observeInputs() {
                ChangeListener<T> listener = (obs, old, val) -> {
                    if(val != null) {
                        emit(val);
                    }
                };
                observable.addListener(listener);
                return () -> observable.removeListener(listener);
            }

            @Override
            protected void newObserver(Consumer<? super T> subscriber) {
                T val = observable.getValue();
                if(val != null) {
                    subscriber.accept(val);
                }
            }
        };
    }

    public static <T> EventStream<Change<T>> changesOf(ObservableValue<T> observable) {
        return new EventStreamBase<Change<T>>() {
            @Override
            protected Subscription observeInputs() {
                ChangeListener<T> listener = (obs, old, val) -> emit(new Change<>(old, val));
                observable.addListener(listener);
                return () -> observable.removeListener(listener);
            }
        };
    }

    /**
     * @see LiveList#changesOf(ObservableList)
     */
    public static <T> EventStream<ListChangeListener.Change<? extends T>> changesOf(ObservableList<T> list) {
        return new EventStreamBase<ListChangeListener.Change<? extends T>>() {
            @Override
            protected Subscription observeInputs() {
                ListChangeListener<T> listener = c -> emit(c);
                list.addListener(listener);
                return () -> list.removeListener(listener);
            }
        };
    }

    /**
     * Use only when the subscriber does not cause {@code list} modification
     * of the underlying list.
     */
    public static <T> EventStream<ListModification<? extends T>> simpleChangesOf(ObservableList<T> list) {
        return new EventStreamBase<ListModification<? extends T>>() {
            @Override
            protected Subscription observeInputs() {
                return LiveList.observeChanges(list, c ->  {
                    for(ListModification<? extends T> mod: c) {
                        emit(mod);
                    }
                });
            }
        };
    }

    public static <T> EventStream<SetChangeListener.Change<? extends T>> changesOf(ObservableSet<T> set) {
        return new EventStreamBase<SetChangeListener.Change<? extends T>>() {
            @Override
            protected Subscription observeInputs() {
                SetChangeListener<T> listener = c -> emit(c);
                set.addListener(listener);
                return () -> set.removeListener(listener);
            }
        };
    }

    public static <K, V> EventStream<MapChangeListener.Change<? extends K, ? extends V>> changesOf(ObservableMap<K, V> map) {
        return new EventStreamBase<MapChangeListener.Change<? extends K, ? extends V>>() {
            @Override
            protected Subscription observeInputs() {
                MapChangeListener<K, V> listener = c -> emit(c);
                map.addListener(listener);
                return () -> map.removeListener(listener);
            }
        };
    }

    public static <C extends Collection<?> & Observable> EventStream<Integer> sizeOf(C collection) {
        return create(() -> collection.size(), collection);
    }

    public static EventStream<Integer> sizeOf(ObservableMap<?, ?> map) {
        return create(() -> map.size(), map);
    }

    private static <T> EventStream<T> create(Supplier<? extends T> computeValue, Observable... dependencies) {
        return new EventStreamBase<T>() {
            private T previousValue;

            @Override
            protected Subscription observeInputs() {
                InvalidationListener listener = obs -> {
                    T value = computeValue.get();
                    if(value != previousValue) {
                        previousValue = value;
                        emit(value);
                    }
                };
                for(Observable dep: dependencies) {
                    dep.addListener(listener);
                }
                previousValue = computeValue.get();

                return () -> {
                    for(Observable dep: dependencies) {
                        dep.removeListener(listener);
                    }
                };
            }

            @Override
            protected void newObserver(Consumer<? super T> subscriber) {
                subscriber.accept(previousValue);
            }
        };
    }

    public static <T extends Event> EventStream<T> eventsOf(
            Node node, EventType<T> eventType) {
        return new EventStreamBase<T>() {
            @Override
            protected Subscription observeInputs() {
                EventHandler<T> handler = this::emit;
                node.addEventHandler(eventType, handler);
                return () -> node.removeEventHandler(eventType, handler);
            }
        };
    }

    public static <T extends Event> EventStream<T> eventsOf(
            Scene scene, EventType<T> eventType) {
        return new EventStreamBase<T>() {
            @Override
            protected Subscription observeInputs() {
                EventHandler<T> handler = this::emit;
                scene.addEventHandler(eventType, handler);
                return () -> scene.removeEventHandler(eventType, handler);
            }
        };
    }

    public static <T extends Event> EventStream<T> eventsOf(
            MenuItem menuItem, EventType<T> eventType) {
        return new EventStreamBase<T>() {
            @Override
            protected Subscription observeInputs() {
                EventHandler<T> handler = this::emit;
                menuItem.addEventHandler(eventType, handler);
                return () -> menuItem.removeEventHandler(eventType, handler);
            }
        };
    }

    public static <T extends Event> EventStream<T> eventsOf(
        Window window, EventType<T> eventType) {
        return new EventStreamBase<T>() {
            @Override
            protected Subscription observeInputs() {
                EventHandler<T> handler = this::emit;
                window.addEventHandler(eventType, handler);
                return () -> window.removeEventHandler(eventType, handler);
            }
        };
    }

    /**
     * Returns an event stream that emits periodic ticks. The first tick
     * is emitted after {@code interval} amount of time has passed.
     * The returned stream may only be used on the JavaFX application thread.
     *
     *
As with all lazily bound streams, ticks are emitted only when there
     * is at least one subscriber to the returned stream. This means that to
     * release associated resources, it suffices to unsubscribe from the
     * returned stream.
     */
    public static EventStream<?> ticks(Duration interval) {
        return new EventStreamBase<Void>() {
            private final Timer timer = FxTimer.createPeriodic(
                    interval, () -> emit(null));

            @Override
            protected Subscription observeInputs() {
                timer.restart();
                return timer::stop;
            }
        };
    }

    /**
     * Returns an event stream that emits periodic ticks. The first tick
     * is emitted at time 0.
     * The returned stream may only be used on the JavaFX application thread.
     *
     *
As with all lazily bound streams, ticks are emitted only when there
     * is at least one subscriber to the returned stream. This means that to
     * release associated resources, it suffices to unsubscribe from the
     * returned stream.
     */
    public static EventStream<?> ticks0(Duration interval) {
        return new EventStreamBase<Void>() {
            private final Timer timer = FxTimer.createPeriodic0(
                    interval, () -> emit(null));

            @Override
            protected Subscription observeInputs() {
                timer.restart();
                return timer::stop;
            }
        };
    }

    /**
     * Returns an event stream that emits periodic ticks on the given
     * {@code eventThreadExecutor}. The returned stream may only be used from
     * that executor's thread.
     *
     *
As with all lazily bound streams, ticks are emitted only when there
     * is at least one subscriber to the returned stream. This means that to
     * release associated resources, it suffices to unsubscribe from the
     * returned stream.
     *
     * @param scheduler scheduler used to schedule periodic emissions.
     * @param eventThreadExecutor single-thread executor used to emit the ticks.
     */
    public static EventStream<?> ticks(
            Duration interval,
            ScheduledExecutorService scheduler,
            Executor eventThreadExecutor) {
        return new EventStreamBase<Void>() {
            private final Timer timer = ScheduledExecutorServiceTimer.createPeriodic(
                    interval, () -> emit(null), scheduler, eventThreadExecutor);

            @Override
            protected Subscription observeInputs() {
                timer.restart();
                return timer::stop;
            }
        };
    }
    /**
     * Returns a {@link #ticks(Duration)} EventStream whose timer restarts whenever
     * impulse emits an event.
     * @param interval - the amount of time that passes until this stream emits its next tick
     * @param impulse - the EventStream that resets this EventStream's internal timer
     */
    public static EventStream<?> restartableTicks(Duration interval, EventStream<?> impulse) {
        return new EventStreamBase<Void>() {
            private final Timer timer = FxTimer.createPeriodic(
                    interval, () -> emit(null));

            @Override
            protected Subscription observeInputs() {
                timer.restart();
                return Subscription.multi(
                        impulse.subscribe(x -> timer.restart()),
                        timer::stop
                );
            }
        };
    }

    /**
     * Returns a {@link #ticks0(Duration)} EventStream whose timer restarts whenever
     * impulse emits an event. Note: since {@link #ticks0(Duration)} is used, restarting
     * the timer will make the returned EventStream immediately emit a new tick.
     * @param interval - the amount of time that passes until this stream emits its next tick
     * @param impulse - the EventStream that resets this EventStream's internal timer
     */
    public static EventStream<?> restartableTicks0(Duration interval, EventStream<?> impulse) {
        return new EventStreamBase<Void>() {
            private final Timer timer = FxTimer.createPeriodic0(
                    interval, () -> emit(null));

            @Override
            protected Subscription observeInputs() {
                timer.restart();
                return Subscription.multi(
                        impulse.subscribe(x -> timer.restart()),
                        timer::stop
                );
            }
        };
    }

    /**
     * Returns an event stream that emits a timestamp of the current frame in
     * nanoseconds on every frame. The timestamp has the same meaning as the
     * argument of the {@link AnimationTimer#handle(long)} method.
     */
    public static EventStream<Long> animationTicks() {
        return new EventStreamBase<Long>() {
            private final AnimationTimer timer = new AnimationTimer() {
                @Override
                public void handle(long now) {
                    emit(now);
                }
            };

            @Override
            protected Subscription observeInputs() {
                timer.start();
                return timer::stop;
            }
        };
    }

    /**
     * Returns a stream that, on each animation frame, emits the duration
     * elapsed since the previous animation frame, in nanoseconds.
     */
    public static EventStream<Long> animationFrames() {
        return animationTicks()
                .accumulate(t(0L, -1L), (state, now) -> state.map((d, last) -> {
                        return t(last == -1L ? 0L : now - last, now);
                }))
                .map(t -> t._1);
    }

    /**
     * Returns an event stream that emits all the events emitted from any of
     * the {@code inputs}. The event type of the returned stream is the nearest
     * common super-type of all the {@code inputs}.
     *
     * @see EventStream#or(EventStream)
     */
    @SafeVarargs
    public static <T> EventStream<T> merge(EventStream<? extends T>... inputs) {
        return new EventStreamBase<T>() {
            @Override
            protected Subscription observeInputs() {
                return Subscription.multi(i -> i.subscribe(this::emit), inputs);
            }
        };
    }

    /**
     * Returns an event stream that emits all the events emitted from any of
     * the event streams in the given observable set. When an event stream is
     * added to the set, the returned stream will start emitting its events.
     * When an event stream is removed from the set, its events will no longer
     * be emitted from the returned stream.
     */
    public static <T> EventStream<T> merge(
            ObservableSet<? extends EventStream<T>> set) {
        return new EventStreamBase<T>() {
            @Override
            protected Subscription observeInputs() {
                return Subscription.dynamic(set, s -> s.subscribe(this::emit));
            }
        };
    }

    /**
     * A more general version of {@link #merge(ObservableSet)} for a set of
     * arbitrary element type and a function to obtain an event stream from
     * the element.
     * @param set observable set of elements
     * @param f function to obtain an event stream from an element
     */
    public static <T, U> EventStream merge(
            ObservableSet<? extends T> set,
            Function<? super T, ? extends EventStream> f) {
        return new EventStreamBase() {
            @Override
            protected Subscription observeInputs() {
                return Subscription.dynamic(
                        set,
                        t -> f.apply(t).subscribe(this::emit));
            }
        };
    }

    public static <L, R> Tuple2<EventStream<L>, EventStream<R>> fork(
            EventStream<? extends Either<L, R>> stream) {
        return t(
                stream.filterMap(Either::asLeft),
                stream.filterMap(Either::asRight));
    }

    public static  EventStream<Tuple2> zip(EventStream srcA, EventStream srcB) {
        return new EventStreamBase<Tuple2>() {
            Pocket pocketA = new ExclusivePocket<>();
            Pocket pocketB = new ExclusivePocket<>();

            @Override
            protected Subscription observeInputs() {
                pocketA.clear();
                pocketB.clear();
                return Subscription.multi(
                        srcA.subscribe(a -> { pocketA.set(a); tryEmit(); }),
                        srcB.subscribe(b -> { pocketB.set(b); tryEmit(); }));
            }

            protected void tryEmit() {
                if(pocketA.hasValue() && pocketB.hasValue()) {
                    emit(t(pocketA.getAndClear(), pocketB.getAndClear()));
                }
            }
        };
    }

    public static  EventStream<Tuple3> zip(EventStream srcA, EventStream srcB, EventStream<C> srcC) {
        return new EventStreamBase<Tuple3>() {
            Pocket pocketA = new ExclusivePocket<>();
            Pocket pocketB = new ExclusivePocket<>();
            Pocket<C> pocketC = new ExclusivePocket<>();

            @Override
            protected Subscription observeInputs() {
                pocketA.clear();
                pocketB.clear();
                pocketC.clear();
                return Subscription.multi(
                        srcA.subscribe(a -> { pocketA.set(a); tryEmit(); }),
                        srcB.subscribe(b -> { pocketB.set(b); tryEmit(); }),
                        srcC.subscribe(c -> { pocketC.set(c); tryEmit(); }));
            }

            protected void tryEmit() {
                if(pocketA.hasValue() && pocketB.hasValue() && pocketC.hasValue()) {
                    emit(t(pocketA.getAndClear(), pocketB.getAndClear(), pocketC.getAndClear()));
                }
            }
        };
    }

    public static  EventStream<Tuple2> combine(
            EventStream srcA,
            EventStream srcB) {
        return new EventStreamBase<Tuple2>() {
            Pocket pocketA = new Pocket<>();
            Pocket pocketB = new Pocket<>();

            @Override
            protected Subscription observeInputs() {
                pocketA.clear();
                pocketB.clear();
                return Subscription.multi(
                        srcA.subscribe(a -> { pocketA.set(a); tryEmit(); }),
                        srcB.subscribe(b -> { pocketB.set(b); tryEmit(); }));
            }

            void tryEmit() {
                if(pocketA.hasValue() && pocketB.hasValue()) {
                    emit(t(pocketA.get(), pocketB.get()));
                }
            }
        };
    }

    public static  EventStream<Tuple3> combine(
            EventStream srcA,
            EventStream srcB,
            EventStream<C> srcC) {
        return new EventStreamBase<Tuple3>() {
            Pocket pocketA = new Pocket<>();
            Pocket pocketB = new Pocket<>();
            Pocket<C> pocketC = new Pocket<>();

            @Override
            protected Subscription observeInputs() {
                pocketA.clear();
                pocketB.clear();
                pocketC.clear();
                return Subscription.multi(
                        srcA.subscribe(a -> { pocketA.set(a); tryEmit(); }),
                        srcB.subscribe(b -> { pocketB.set(b); tryEmit(); }),
                        srcC.subscribe(c -> { pocketC.set(c); tryEmit(); }));
            }

            void tryEmit() {
                if(pocketA.hasValue() && pocketB.hasValue() && pocketC.hasValue()) {
                    emit(t(pocketA.get(), pocketB.get(), pocketC.get()));
                }
            }
        };
    }

    public static  EventStream<Tuple4> combine(
            EventStream srcA,
            EventStream srcB,
            EventStream<C> srcC,
            EventStream<D> srcD) {
        return new EventStreamBase<Tuple4>() {
            Pocket pocketA = new Pocket<>();
            Pocket pocketB = new Pocket<>();
            Pocket<C> pocketC = new Pocket<>();
            Pocket<D> pocketD = new Pocket<>();

            @Override
            protected Subscription observeInputs() {
                pocketA.clear();
                pocketB.clear();
                pocketC.clear();
                pocketD.clear();
                return Subscription.multi(
                        srcA.subscribe(a -> { pocketA.set(a); tryEmit(); }),
                        srcB.subscribe(b -> { pocketB.set(b); tryEmit(); }),
                        srcC.subscribe(c -> { pocketC.set(c); tryEmit(); }),
                        srcD.subscribe(d -> { pocketD.set(d); tryEmit(); }));
            }

            void tryEmit() {
                if(pocketA.hasValue() && pocketB.hasValue()
                        && pocketC.hasValue() && pocketD.hasValue()) {

                    emit(t(pocketA.get(), pocketB.get(),
                            pocketC.get(), pocketD.get()));
                }
            }
        };
    }

    public static  EventStream<Tuple5> combine(
            EventStream srcA,
            EventStream srcB,
            EventStream<C> srcC,
            EventStream<D> srcD,
            EventStream<E> srcE) {
        return new EventStreamBase<Tuple5>() {
            Pocket pocketA = new Pocket<>();
            Pocket pocketB = new Pocket<>();
            Pocket<C> pocketC = new Pocket<>();
            Pocket<D> pocketD = new Pocket<>();
            Pocket<E> pocketE = new Pocket<>();

            @Override
            protected Subscription observeInputs() {
                pocketA.clear();
                pocketB.clear();
                pocketC.clear();
                pocketD.clear();
                pocketE.clear();
                return Subscription.multi(
                        srcA.subscribe(a -> { pocketA.set(a); tryEmit(); }),
                        srcB.subscribe(b -> { pocketB.set(b); tryEmit(); }),
                        srcC.subscribe(c -> { pocketC.set(c); tryEmit(); }),
                        srcD.subscribe(d -> { pocketD.set(d); tryEmit(); }),
                        srcE.subscribe(e -> { pocketE.set(e); tryEmit(); }));
            }

            void tryEmit() {
                if(pocketA.hasValue() && pocketB.hasValue()
                        && pocketC.hasValue() && pocketD.hasValue()
                        && pocketE.hasValue()) {

                    emit(t(pocketA.get(), pocketB.get(), pocketC.get(),
                            pocketD.get(), pocketE.get()));
                }
            }
        };
    }

    public static  EventStream<Tuple6> combine(
            EventStream srcA,
            EventStream srcB,
            EventStream<C> srcC,
            EventStream<D> srcD,
            EventStream<E> srcE,
            EventStream<F> srcF) {
        return new EventStreamBase<Tuple6>() {
            Pocket pocketA = new Pocket<>();
            Pocket pocketB = new Pocket<>();
            Pocket<C> pocketC = new Pocket<>();
            Pocket<D> pocketD = new Pocket<>();
            Pocket<E> pocketE = new Pocket<>();
            Pocket<F> pocketF = new Pocket<>();

            @Override
            protected Subscription observeInputs() {
                pocketA.clear();
                pocketB.clear();
                pocketC.clear();
                pocketD.clear();
                pocketE.clear();
                pocketF.clear();
                return Subscription.multi(
                        srcA.subscribe(a -> { pocketA.set(a); tryEmit(); }),
                        srcB.subscribe(b -> { pocketB.set(b); tryEmit(); }),
                        srcC.subscribe(c -> { pocketC.set(c); tryEmit(); }),
                        srcD.subscribe(d -> { pocketD.set(d); tryEmit(); }),
                        srcE.subscribe(e -> { pocketE.set(e); tryEmit(); }),
                        srcF.subscribe(f -> { pocketF.set(f); tryEmit(); }));
            }

            void tryEmit() {
                if(pocketA.hasValue() && pocketB.hasValue()
                        && pocketC.hasValue() && pocketD.hasValue()
                        && pocketE.hasValue() && pocketF.hasValue()) {

                    emit(t(pocketA.get(), pocketB.get(), pocketC.get(),
                            pocketD.get(), pocketE.get(), pocketF.get()));
                }
            }
        };
    }

    private static class Pocket<T> {
        private boolean hasValue = false;
        private T value = null;

        public boolean hasValue() { return hasValue; }
        public void set(T value) {
            this.value = value;
            hasValue = true;
        }
        public T get() {
            return value;
        }
        public void clear() {
            hasValue = false;
            value = null;
        }
        public T getAndClear() {
            T res = get();
            clear();
            return res;
        }
    }

    private static class ExclusivePocket<T> extends Pocket<T> {
        @Override
        public final void set(T a) {
            if(hasValue()) {
                throw new IllegalStateException("Value arrived out of order: " + a);
            } else {
                super.set(a);
            }
        };
    }
}


package org.reactfx.inhibeans.collection;

import org.reactfx.collection.LiveList;

@Deprecated
public final class Collections {

    // private constructor to prevent instantiation
    private Collections() {}

    /**
     * Creates an ObservableList wrapper that is able to temporarily block
     * list change notifications.
     * @param delegate the underlying observable list.
     * @deprecated Use {@link LiveList#suspendable(javafx.collections.ObservableList)} instead.
     */
    @Deprecated
    public static <E> ObservableList<E> wrap(javafx.collections.ObservableList<E> delegate) {
        return LiveList.suspendable(delegate);
    }
}