text: import get from 'lodash.get';
  import set from 'lodash.set';
  import omit from 'lodash.omit';
  import isUndefined from 'lodash.isundefined';
  import isPlainObject from 'lodash.isplainobject';
  import isFunction from 'lodash.isfunction';
  import cloneDeep from 'lodash.clonedeep';
  import TinyQueue from 'tinyqueue';
  import {
    defaultMainColor,
    defaultObservableColor,
    defaultShapeColor
  } from './colors';
  
  const isObservable = value =>
    isPlainObject(value) &&
    value.type === 'observable' &&
    Array.isArray(value.values);
  const isError = value => isPlainObject(value) && !isUndefined(value.error);
  const isCompleted = value => isPlainObject(value) && value.completed === true;
  const isMeta = value => isPlainObject(value) && value.meta === true;
  
  export const isTimeout = value => isMeta(value) && value.timeout === true;
  
  const colorizeItem = ({ isObservable, item }) => {
    if (typeof item.color === 'string') {
      return item;
    }
  
    return {
      ...item,
      color: isObservable ? defaultObservableColor : defaultShapeColor
    };
  };
  
  const prepareItem = ({
    isObservable,
    isError,
    item,
    observableIndex,
    valueIndex,
    renderer
  }) => {
    if (isFunction(renderer)) {
      const { value, ...rest } = item;
      const renderedItem = renderer({
        isObservable,
        isError,
        value: isError ? item.error : value,
        observableIndex,
        valueIndex
      });
      const color = isError
        ? {}
        : { color: isObservable ? defaultObservableColor : defaultShapeColor };
  
      return {
        ...color,
        ...rest,
        ...renderedItem
      };
    }
  
    return isError ? omit(item, 'error') : colorizeItem({ isObservable, item });
  };
  
  export const updateData = (observable, path, value) => {
    const result = cloneDeep(observable);
    const dataPath = path.reduce((acc, index) => acc.concat(index, 'values'), [
      'values'
    ]);
  
    return set(result, dataPath, get(result, dataPath).concat(value));
  };
  
  const getTextForMergedTooltip = value => {
    const { tooltip } = value;
  
    if (tooltip && tooltip.text) {
      return tooltip.text;
    }
  
    return value.text;
  };
  
  const calcMergedTooltip = mergedTooltip => ({
    text: mergedTooltip.join(', ')
  });
  
  export const getModel = ({
    data,
    renderer,
    inheritMainColor = true,
    mergeThreshold
  }) => {
    let result = {
      observables: [],
      connectors: []
    };
  
    if (!isObservable(data)) {
      return result;
    }
  
    let queue = new TinyQueue(
      [
        {
          observable: data,
          startTime: 0,
          mainColor: defaultMainColor
        }
      ],
      (obs1, obs2) => obs1.startTime - obs2.startTime
    );
  
    while (queue.length > 0) {
      const { observable, startTime, mainColor, fromIndex } = queue.pop();
      const observableIndex = result.observables.length;
  
      if (!isUndefined(fromIndex)) {
        result.connectors.push({
          time: startTime,
          fromIndex,
          toIndex: observableIndex,
          color: mainColor
        });
      }
  
      let resultObservable = {
        values: [],
        startTime: startTime,
        mainColor: mainColor
      };
  
      for (let i = 0, len = observable.values.length; i < len; i++) {
        const item = observable.values[i];
        const valueIndex = resultObservable.values.length;
  
        if (isObservable(item)) {
          const { time } = item;
          const value = prepareItem({
            isObservable: true,
            isError: false,
            item: {
              ...omit(item, 'type', 'values'),
              isObservable: true,
              text: ''
            },
            observableIndex,
            valueIndex,
            renderer
          });
          const { color } = value;
  
          resultObservable.values.push(value);
  
          queue.push({
            observable: item,
            startTime: time,
            mainColor: inheritMainColor ? color : defaultMainColor,
            fromIndex: observableIndex
          });
        } else if (isError(item)) {
          resultObservable.endTime = item.time;
          resultObservable.error = prepareItem({
            isObservable: false,
            isError: true,
            item,
            observableIndex,
            valueIndex,
            renderer
          });
        } else if (isCompleted(item)) {
          resultObservable.endTime = item.time;
          resultObservable.completed = {
            time: item.time
          };
  
          const valuesCount = resultObservable.values.length;
  
          if (valuesCount > 0) {
            resultObservable.completed.lastValueBeforeCompletedTime =
              resultObservable.values[valuesCount - 1].time;
          }
        } else {
          const value = prepareItem({
            isObservable: false,
            isError: false,
            item,
            observableIndex,
            valueIndex,
            renderer
          });
          const lastValue =
            valueIndex === 0 ? null : resultObservable.values[valueIndex - 1];
  
          if (
            !mergeThreshold ||
            !lastValue ||
            value.time - lastValue.time > mergeThreshold
          ) {
            resultObservable.values.push(value);
          } else {
            if (typeof lastValue.count === 'number') {
              lastValue.count += 1;
              lastValue.mergedTooltip.push(getTextForMergedTooltip(value));
            } else {
              /* First merge */
              lastValue.mergedTooltip = [
                getTextForMergedTooltip(lastValue),
                getTextForMergedTooltip(value)
              ];
              lastValue.text = '...'; // Note that we change the text AFTER it's passed to lastValue.mergedTooltip
              lastValue.count = 2;
            }
          }
        }
      }
  
      resultObservable.values.forEach(value => {
        if (value.mergedTooltip) {
          value.tooltip = calcMergedTooltip(value.mergedTooltip);
  
          delete value.mergedTooltip;
          delete value.textStyle;
        }
      });
  
      result.observables.push(resultObservable);
    }
  
    return result;
  };
  