import { PatchPack } from "patchpack"

import { ITreeNode, IReversibleJsonPatch, mx } from "../internal"
import { Serializer } from "."
import { values } from "mobx"

export interface ILightSchema {
  [name: string]: ISchemaItem
}

export interface ISchemaItem {
  index: number
  parent: string
  props: string[]
  schema: IPropsSchema
}

export interface IPropsSchema {
  [prop: string]: string | IPropSchema
}

export interface IPropSchema {
  type: "map" | "set" | "array"
  items: string
}

export class LightSerializer extends Serializer {
  private _schema: ILightSchema = {}

  public onCreate() {
    const context = mx.$context

    const propType = (prop: any) => {
      if (typeof prop.type === "string") {
        return prop.type
      } else if (Array.isArray(prop.type)) {
        return {
          type: "array",
          items: prop.type.map.name,
        }
      } else if (typeof prop.type === "function") {
        return prop.type.name
      } else if (typeof prop.type === "object") {
        return {
          type: "map",
          items: prop.type.map.name,
        }
      }
    }

    context.types.forEach(( { name, $mx } ) => {
      const propSchema: IPropsSchema = {}

      $mx.props.forEach((prop: any) => {
        propSchema[prop.key] = propType(prop)
      })

      const meta = context.meta.get(name)
      if (!meta) { return }

      this._schema[name] = {
        index: meta.index,
        parent: meta.parent,
        props: $mx.props.map((prop: any) => prop.key),
        schema: propSchema,
      }
    })
  }

  private entryTypePath (entry?: ITreeNode): string[] {
    const pathArr = []
    while (entry) {
      pathArr.push(entry.meta && entry.meta.type || "")
      entry = entry.parent
    }
    return pathArr.reverse()
  }

 /**
  * light compression
  * |      |  0 |  1 | 2...n+2 |                     n+3... |
  * |------|----|----|---------|----------------------------|
  * | byte | op | n  |    path | [params, value, oldValue]* |
  *
  * op:
  *  0 -> "add"
  *  1 -> "replace"
  *  2 -> "remove"
  *
  * n: length of path
  *
  * path:
  *  first byte >= 0 -> typeIndex, second byte -> propIndex
  *  first byte < 0  -> param
  *
  * "*" - encoded with notepack
  */

  public encodePatch(patch: IReversibleJsonPatch, entry: ITreeNode): Buffer {
    const pathTypes = this.entryTypePath(entry)

    const op = ["add", "replace", "remove"].indexOf(patch.op)

    const pathArr = patch.path.replace(/^\/+|\/+$/g, "").split("/")
    const pathBuffer: number[] = []
    const pathParams: any[] = []
    pathArr.forEach((key, i) => {
      const typeName = pathTypes[i]
      if (!typeName) {
        pathBuffer.push(-1)
        pathParams.push(key)
      } else {
        pathBuffer.push(...this.typePropIndex(typeName, key))
      }
    })

    // tslint:disable-next-line: no-bitwise
    const buffer = Buffer.from([op, pathBuffer.length, ...pathBuffer])
    if (patch.op !== "remove") {
      pathParams.push(patch.value)
    }
    if (patch.op !== "add" && "oldValue" in patch) {
      pathParams.push(patch.oldValue)
    }
    const paramsBuffer = Buffer.from(PatchPack.encode(pathParams))

    return Buffer.concat([buffer, paramsBuffer])
  }

  private typePropIndex (type: string, prop: string) {

    const meta = mx.$context.meta.get(type)
    if (!meta) {
      throw new Error(`Cannot get prop ${prop} index - object type ${type} not found!`)
    }

    const props: any[] = meta.ref.$mx.props
    const index = props.findIndex(({ key }) => key === prop)

    return [meta.index, index]
  }

  public decodePatch (buffer: Buffer): IReversibleJsonPatch {
    const opIndex = buffer.readInt8(0)
    const pathLength = buffer.readInt8(1)

    const pathBuffer = buffer.slice(2, pathLength + 2)
    const paramsBuffer = buffer.slice(pathLength + 2)

    const params = PatchPack.decode(paramsBuffer).reverse()

    let path = ""
    for (let i = 0; i < pathBuffer.length; i++) {
      const typeIndex = pathBuffer.readInt8(i)
      if (typeIndex < 0) {
        path += "/" + params.pop()
      } else {
        const propIndex = pathBuffer.readInt8(++i)
        path += "/" + this.propName(typeIndex, propIndex)
      }
    }

    const patch: any = {
      op: ["add", "replace", "remove"][opIndex],
      path,
    }

    if (params.length && patch.op !== "remove") {
      patch.value = params.pop()
    }

    if (params.length && patch.op !== "add") {
      patch.oldValue = params.pop()
    }

    if (params.length) {
      throw new Error(`Unhandled params: ${params.toString()}`)
    }

    return patch
  }

  private propName(typeIndex: number, propIndex: number) {
    const context = mx.$context
    const typeName = context.index[typeIndex]
    const meta = context.meta.get(typeName)

    if (!meta) {
      throw new Error(`Cannot get prop ${propIndex} name - object type ${typeName} not found!`)
    }

    const prop: any = meta.ref.$mx.props[propIndex]

    if (!prop) {
      throw new Error(`Cannot get prop ${propIndex} name - property not found!`)
    }

    return prop.key
  }

  public encodeSnapshot(value: any): Buffer {
    return PatchPack.encode([this._schema, values])
  }

  public decodeSnapshot(buffer: Buffer): any {
    const [schema, snapshot] = PatchPack.decode(buffer)
    this._schema = schema
    return snapshot
  }
}
