// from https://gist.github.com/msteen/e4828fbf25d6efef73576fc43ac479d2
// https://discuss.codemirror.net/t/whats-the-best-to-test-and-debug-grammars/2542/5
// MIT License
//
// Copyright (c) 2021 Matthijs Steen
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import { Text } from '@codemirror/state'
import { Tree, TreeCursor } from '@lezer/common'

class StringInput {
  constructor(input) {
    this.input = input
    this.lineChunks = false
  }

  get length() {
    return this.input.length
  }

  chunk(from) {
    return this.input.slice(from)
  }

  read(from, to) {
    return this.input.slice(from, to)
  }
}

function cursorNode({ type, from, to }, isLeaf = false) {
  return { type, from, to, isLeaf }
}
function traverseTree(
  cursor,
  {
    from = -Infinity,
    to = Infinity,
    includeParents = false,
    beforeEnter,
    onEnter,
    onLeave,
  }
) {
  if (!(cursor instanceof TreeCursor))
    cursor = cursor instanceof Tree ? cursor.cursor() : cursor.cursor()
  for (;;) {
    let node = cursorNode(cursor)
    let leave = false
    if (node.from <= to && node.to >= from) {
      const enter =
        !node.type.isAnonymous &&
        (includeParents || (node.from >= from && node.to <= to))
      if (enter && beforeEnter) beforeEnter(cursor)
      node.isLeaf = !cursor.firstChild()
      if (enter) {
        leave = true
        if (onEnter(node) === false) return
      }
      if (!node.isLeaf) continue
    }
    for (;;) {
      node = cursorNode(cursor, node.isLeaf)
      if (leave && onLeave) if (onLeave(node) === false) return
      leave = cursor.type.isAnonymous
      node.isLeaf = false
      if (cursor.nextSibling()) break
      if (!cursor.parent()) return
      leave = true
    }
  }
}
function isChildOf(child, parent) {
  return (
    child.from >= parent.from &&
    child.from <= parent.to &&
    child.to <= parent.to &&
    child.to >= parent.from
  )
}
function validatorTraversal(input, { fullMatch = true } = {}) {
  if (typeof input === 'string') input = new StringInput(input)
  const state = {
    valid: true,
    parentNodes: [],
    lastLeafTo: 0,
  }
  return {
    state,
    traversal: {
      onEnter(node) {
        state.valid = true
        if (!node.isLeaf) state.parentNodes.unshift(node)
        if (node.from > node.to || node.from < state.lastLeafTo) {
          state.valid = false
        } else if (node.isLeaf) {
          if (
            state.parentNodes.length &&
            !isChildOf(node, state.parentNodes[0])
          )
            state.valid = false
          state.lastLeafTo = node.to
        } else {
          if (state.parentNodes.length) {
            if (!isChildOf(node, state.parentNodes[0])) state.valid = false
          } else if (
            fullMatch &&
            (node.from !== 0 || node.to !== input.length)
          ) {
            state.valid = false
          }
        }
      },
      onLeave(node) {
        if (!node.isLeaf) state.parentNodes.shift()
      },
    },
  }
}

let Color
;(function (Color) {
  Color[(Color.Red = 31)] = 'Red'
  Color[(Color.Green = 32)] = 'Green'
  Color[(Color.Yellow = 33)] = 'Yellow'
})(Color || (Color = {}))

function colorize(value, color) {
  return '\u001b[' + color + 'm' + String(value) + '\u001b[39m'
}

function printTree(
  cursor,
  input,
  { from, to, start = 0, includeParents } = {}
) {
  const inp = typeof input === 'string' ? new StringInput(input) : input
  const text = Text.of(inp.read(0, inp.length).split('\n'))
  const state = {
    output: '',
    prefixes: [],
    hasNextSibling: false,
  }
  const validator = validatorTraversal(inp)
  traverseTree(cursor, {
    from,
    to,
    includeParents,
    beforeEnter(cursor) {
      state.hasNextSibling = cursor.nextSibling() && cursor.prevSibling()
    },
    onEnter(node) {
      validator.traversal.onEnter(node)
      const isTop = state.output === ''
      const hasPrefix = !isTop || node.from > 0
      if (hasPrefix) {
        state.output += (!isTop ? '\n' : '') + state.prefixes.join('')
        if (state.hasNextSibling) {
          state.output += ' ├─ '
          state.prefixes.push(' │  ')
        } else {
          state.output += ' └─ '
          state.prefixes.push('    ')
        }
      }
      const hasRange = node.from !== node.to
      state.output +=
        (node.type.isError || !validator.state.valid
          ? colorize('ERROR ' + node.type.name, Color.Red)
          : node.type.name) +
        ' ' +
        (hasRange
          ? '[' +
            colorize(locAt(text, start + node.from), Color.Yellow) +
            '..' +
            colorize(locAt(text, start + node.to), Color.Yellow) +
            ']'
          : colorize(locAt(text, start + node.from), Color.Yellow))
      if (hasRange && node.isLeaf) {
        state.output +=
          ': ' +
          colorize(JSON.stringify(inp.read(node.from, node.to)), Color.Green)
      }
    },
    onLeave(node) {
      validator.traversal.onLeave(node)
      state.prefixes.pop()
    },
  })
  return state.output
}

function locAt(text, pos) {
  const line = text.lineAt(pos)
  return line.number + ':' + (pos - line.from)
}

export function logTree(tree, input, options) {
  console.warn(printTree(tree, input, options))
}
