# model/InkModel.js

    import { modelLogger as logger } from '../configuration/LoggerConfig'
    import * as StrokeComponent from './StrokeComponent'
    import { getSymbolsBounds } from './Symbol'

    /**
     * Recognition positions
     * @typedef {Object} RecognitionPositions
     * @property {Number} [lastSentPosition=-1] Index of the last sent stroke.
     * @property {Number} [lastReceivedPosition=-1] Index of the last received stroke.
     * @property {Number} [lastRenderedPosition=-1] Last rendered recognized symbol position
     */

    /**
     * Raw results
     * @typedef {Object} RawResults
     * @property {Object} convert=undefined The convert result
     * @property {Object} exports=undefined The exports output as return by the recognition service.
     */

    /**
     * Editor model
     * @typedef {Object} Model
     * @property {Stroke} currentStroke=undefined Stroke in building process.
     * @property {Array<Stroke>} rawStrokes=[] List of captured strokes.
     * @property {Array} strokeGroups=[] Group of strokes with same pen style.
     * @property {RecognitionPositions} lastPositions Last recognition sent/received stroke indexes.
     * @property {Array<Object>} defaultSymbols=[] Default symbols, relative to the current recognition type.
     * @property {Array<Object>} recognizedSymbols=undefined Symbols to render (e.g. stroke, shape primitives, string, characters...).
     * @property {Object} exports=undefined Result of the export (e.g. mathml, latex, text...).
     * @property {RawResults} rawResults The recognition output as return by the recognition service.
     * @property {Number} creationTime Date of creation timestamp.
     * @property {Number} modificationTime=undefined Date of lastModification.
     */

    /**
     * Bounding box
     * @typedef {Object} Bounds
     * @property {Number} minX Minimal x coordinate
     * @property {Number} maxX Maximal x coordinate
     * @property {Number} minY Minimal y coordinate
     * @property {Number} maxY Maximal y coordinate
     */

    /**
     * Create a new model
     * @param {Configuration} [configuration] Parameters to use to populate default recognition symbols
     * @return {Model} New model
     */
    export function createModel (configuration) {
      // see @typedef documentation on top
      return {
        currentStroke: undefined,
        rawStrokes: [],
        strokeGroups: [],
        lastPositions: {
          lastSentPosition: -1,
          lastReceivedPosition: -1,
          lastRenderedPosition: -1
        },
        defaultSymbols: [],
        recognizedSymbols: undefined,
        exports: undefined,
        rawResults: {
          convert: undefined,
          exports: undefined
        },
        creationTime: new Date().getTime(),
        modificationTime: undefined
      }
    }

    /**
     * Clear the model.
     * @param {Model} model Current model
     * @return {Model} Cleared model
     */
    export function clearModel (model) {
      const modelReference = model
      modelReference.currentStroke = undefined
      modelReference.rawStrokes = []
      modelReference.strokeGroups = []
      modelReference.lastPositions.lastSentPosition = -1
      modelReference.lastPositions.lastReceivedPosition = -1
      modelReference.lastPositions.lastRenderedPosition = -1
      modelReference.recognizedSymbols = undefined
      modelReference.exports = undefined
      modelReference.rawResults.convert = undefined
      modelReference.rawResults.exports = undefined
      return modelReference
    }

    /**
     * Check if the model needs to be redrawn.
     * @param {Model} model Current model
     * @return {Boolean} True if the model needs to be redrawn, false otherwise
     */
    export function needRedraw (model) {
      return model.recognizedSymbols ? (model.rawStrokes.length !== model.recognizedSymbols.filter(symbol => symbol.type === 'stroke').length) : false
    }

    /**
     * Mutate the model given in parameter by adding the new strokeToAdd.
     * @param {Model} model Current model
     * @param {Stroke} stroke Stroke to be added to pending ones
     * @return {Model} Updated model
     */
    export function addStroke (model, stroke) {
      // We use a reference to the model. The purpose here is to update the pending stroke only.
      const modelReference = model
      logger.debug('addStroke', stroke)
      modelReference.rawStrokes.push(stroke)
      return modelReference
    }

    /**
     * Mutate the model given in parameter by adding the new strokeToAdd and the penstyle. Used for iink REST.
     * @param {Model} model Current model
     * @param {Stroke} stroke Stroke to be added to pending ones
     * @param {PenStyle} strokePenStyle
     * @return {Model} Updated model
     */
    export function addStrokeToGroup (model, stroke, strokePenStyle) {
      // We use a reference to the model. The purpose here is to update the pending stroke only.
      const modelReference = model
      logger.debug('addStroke', stroke)
      const lastGroup = modelReference.strokeGroups.length - 1
      if (modelReference.strokeGroups[lastGroup] && modelReference.strokeGroups[lastGroup].penStyle === strokePenStyle) {
        modelReference.strokeGroups[lastGroup].strokes.push(stroke)
      } else {
        const newStrokeGroup = {
          penStyle: strokePenStyle,
          strokes: []
        }
        const strokeCopy = {}
        Object.assign(strokeCopy, stroke)
        newStrokeGroup.strokes.push(strokeCopy)
        modelReference.strokeGroups.push(newStrokeGroup)
      }
      return modelReference
    }

    /**
     * Get the strokes that needs to be recognized
     * @param {Model} model Current model
     * @param {Number} [position=lastReceived] Index from where to extract strokes
     * @return {Array<Stroke>} Pending strokes
     */
    export function extractPendingStrokes (model, position = model.lastPositions.lastReceivedPosition + 1) {
      return model.rawStrokes.slice(position)
    }

    /**
     * Mutate the model by adding a point and close the current stroke.
     * @param {Model} model Current model
     * @param {{x: Number, y: Number, t: Number}} point Captured point to create current stroke
     * @param {Object} properties Properties to be applied to the current stroke
     * @param {Number} [dpi=96] The screen dpi resolution
     * @return {Model} Updated model
     */
    export function initPendingStroke (model, point, properties, dpi = 96) {
      if (properties && properties['-myscript-pen-width']) {
        const pxWidth = (properties['-myscript-pen-width'] * dpi) / 25.4
        Object.assign(properties, { width: pxWidth / 2 }) // FIXME hack to get better render
      }
      const modelReference = model
      logger.trace('initPendingStroke', point)
      // Setting the current stroke to an empty one
      modelReference.currentStroke = StrokeComponent.createStrokeComponent(properties)
      modelReference.currentStroke = StrokeComponent.addPoint(modelReference.currentStroke, point)
      return modelReference
    }

    /**
     * Mutate the model by adding a point to the current pending stroke.
     * @param {Model} model Current model
     * @param {{x: Number, y: Number, t: Number}} point Captured point to be append to the current stroke
     * @return {Model} Updated model
     */
    export function appendToPendingStroke (model, point) {
      const modelReference = model
      if (modelReference.currentStroke) {
        logger.trace('appendToPendingStroke', point)
        modelReference.currentStroke = StrokeComponent.addPoint(modelReference.currentStroke, point)
      }
      return modelReference
    }

    /**
     * Mutate the model by adding the new point on a initPendingStroke.
     * @param {Model} model Current model
     * @param {{x: Number, y: Number, t: Number}} point Captured point to be append to the current stroke
     * @param {PenStyle} penStyle
     * @return {Model} Updated model
     */
    export function endPendingStroke (model, point, penStyle) {
      const modelReference = model
      if (modelReference.currentStroke) {
        logger.trace('endPendingStroke', point)
        const currentStroke = StrokeComponent.addPoint(modelReference.currentStroke, point)
        // Mutating pending strokes
        addStroke(modelReference, currentStroke)
        addStrokeToGroup(modelReference, currentStroke, penStyle)
        // Resetting the current stroke to an undefined one
        delete modelReference.currentStroke
      }
      return modelReference
    }

    /**
     * Get the bounds of the current model.
     * @param {Model} model Current model
     * @return {Bounds} Bounding box enclosing the current drawn model
     */
    export function getBorderCoordinates (model) {
      let modelBounds = { minX: Number.MAX_VALUE, maxX: Number.MIN_VALUE, minY: Number.MAX_VALUE, maxY: Number.MIN_VALUE }

      // Default symbols
      if (model.defaultSymbols && model.defaultSymbols.length > 0) {
        modelBounds = getSymbolsBounds(model.defaultSymbols, modelBounds)
      }
      // Recognized symbols
      if (model.recognizedSymbols && model.recognizedSymbols.length > 0) {
        modelBounds = getSymbolsBounds(model.recognizedSymbols, modelBounds)
        // Pending strokes
        modelBounds = getSymbolsBounds(extractPendingStrokes(model), modelBounds)
      } else {
        modelBounds = getSymbolsBounds(model.rawStrokes, modelBounds)
      }
      return modelBounds
    }

    /**
     * Extract strokes from an ink range
     * @param {Model} model Current model
     * @param {Number} firstStroke First stroke index to extract
     * @param {Number} lastStroke Last stroke index to extract
     * @param {Number} firstPoint First point index to extract
     * @param {Number} lastPoint Last point index to extract
     * @return {Array<Stroke>} The extracted strokes
     */
    export function extractStrokesFromInkRange (model, firstStroke, lastStroke, firstPoint, lastPoint) {
      return model.rawStrokes.slice(firstStroke, lastStroke + 1).map((stroke, index, slicedStrokes) => {
        if (slicedStrokes.length < 2) {
          return StrokeComponent.slice(stroke, firstPoint, lastPoint + 1)
        }
        if (index === 0) {
          return StrokeComponent.slice(stroke, firstPoint)
        }
        if (index === (slicedStrokes.length - 1)) {
          return StrokeComponent.slice(stroke, 0, lastPoint + 1)
        }
        return stroke
      })
    }

    /**
     * Update model lastSentPosition
     * @param {Model} model
     * @param {Number} [position]
     * @return {Model}
     */
    export function updateModelSentPosition (model, position = model.rawStrokes.length - 1) {
      const modelReference = model
      modelReference.lastPositions.lastSentPosition = position
      return modelReference
    }

    /**
     * Update model lastReceivedPosition regarding to lastSentPosition
     * @param {Model} model
     * @return {Model}
     */
    export function updateModelReceivedPosition (model) {
      const modelReference = model
      modelReference.lastPositions.lastReceivedPosition = modelReference.lastPositions.lastSentPosition
      return modelReference
    }

    /**
     * Reset model lastReceivedPosition and lastSentPosition
     * @param {Model} model
     * @return {Model}
     */
    export function resetModelPositions (model) {
      const modelReference = model
      modelReference.lastPositions.lastSentPosition = -1
      modelReference.lastPositions.lastReceivedPosition = -1
      return modelReference
    }

    /**
     * Reset model lastRenderedPosition
     * @param {Model} model
     * @return {Model}
     */
    export function resetModelRendererPosition (model) {
      const modelReference = model
      modelReference.lastPositions.lastRenderedPosition = -1
      return modelReference
    }

    /**
     * Update model lastRenderedPosition
     * @param {Model} model
     * @param {Number} [position]
     * @return {Model}
     */
    export function updateModelRenderedPosition (model, position = model.recognizedSymbols ? model.recognizedSymbols.length - 1 : -1) {
      const modelReference = model
      modelReference.lastPositions.lastRenderedPosition = position
      return modelReference
    }

    /**
     * Get the symbols that needs to be rendered
     * @param {Model} model Current model
     * @param {Number} [position=lastRendered] Index from where to extract symbols
     * @return {Array<Object>}
     */
    export function extractPendingRecognizedSymbols (model, position = model.lastPositions.lastRenderedPosition + 1) {
      return model.recognizedSymbols ? model.recognizedSymbols.slice(position) : []
    }

    /**
     * Clone model
     * @param {Model} model Current model
     * @return {Model} Clone of the current model
     */
    export function cloneModel (model) {
      const clonedModel = Object.assign({}, model)
      // We clone the properties that need to be. Take care of arrays.
      clonedModel.defaultSymbols = [...model.defaultSymbols]
      clonedModel.currentStroke = model.currentStroke ? Object.assign({}, model.currentStroke) : undefined
      clonedModel.rawStrokes = [...model.rawStrokes]
      clonedModel.strokeGroups = JSON.parse(JSON.stringify(model.strokeGroups))
      clonedModel.lastPositions = Object.assign({}, model.lastPositions)
      clonedModel.exports = model.exports ? Object.assign({}, model.exports) : undefined
      clonedModel.rawResults = Object.assign({}, model.rawResults)
      clonedModel.recognizedSymbols = model.recognizedSymbols ? [...model.recognizedSymbols] : undefined
      return clonedModel
    }

    /**
     * Merge models
     * @param {...Model} models Models to merge (ordered)
     * @return {Model} Updated model
     */
    export function mergeModels (...models) {
      return models.reduce((a, b) => {
        const modelRef = a
        modelRef.recognizedSymbols = b.recognizedSymbols
        modelRef.lastPositions.lastSentPosition = b.lastPositions.lastSentPosition
        modelRef.lastPositions.lastReceivedPosition = b.lastPositions.lastReceivedPosition
        modelRef.lastPositions.lastRenderedPosition = b.lastPositions.lastRenderedPosition
        modelRef.rawResults = b.rawResults
        modelRef.exports = b.exports
        return modelRef
      })
    }