import { Chord } from './Chord'
import { Diagram } from './Diagram'
import { SvgDiagram } from './SvgDiagram'

/**
 * group 0: match
 * group 1: chordShortName
 * group 2: chordName
 * group 3: chordVariant
 * group 4: chordDiagram
 * group 5: onlyDiagram
 * group 6: space until next diagram/comment/linebreak
 * group 7: comment length (if any)
 */
const regexChordName = '([A-G](?:[b#♭♯Øº∆()+-]|[2-9]|1[0-3]|[Mm](?:aj)?|add|s(?:us)?|aug|d(?:im)?)*(?:\\/[A-G][b#♭♯]?)?)'
const regexChordShortName = regexChordName
const regexChordLongName = regexChordName
const chordsRegExFields = {
  pattern: `(?:(?:\\b|^)(?:${regexChordShortName}\\|)?(?:${regexChordLongName}(?::(\\w+))?(?:\\[([x:/,\\-0-9]+)\\])?)|(?:\\[([x:/,\\-0-9]+)\\]))([\\t ]*)(\\/\\/.*)?`,
  flags: 'g'
}
const chordLinesRegExFields = {
  pattern: `^[\\t ]*(${chordsRegExFields.pattern})+[\\t ]*$`,
  flags: 'gm'
}
// export const chordsRegEx: RegExp = /(?:(?:\b|^)(?:([A-G](?:[b#♭♯Øº∆()+-]|[2-9]|1[0-3]|M(?:aj)?|m(?:aj)?|add|sus|aug|dim)*(?:\/[A-G][b#♭♯]?)?)(?::(\w+))?(?:\[([x:/,\-0-9]+)\])?)|(?:\[([x:/,\-0-9]+)\]))([\t ]*)/g
// export const chordLineRegEx: RegExp = /^([\t ]*(?:(?:\b|^)(?:([A-G](?:[b#♭♯Øº∆()+-]|[2-9]|1[0-3]|M(?:aj)?|m(?:aj)?|add|sus|aug|dim)*(?:\/[A-G][b#♭♯]?)?)(?::(\w+))?(?:\[([x:/,\-0-9]+)\])?)|(?:\[([x:/,\-0-9]+)\]))[\t ]*)+$/gm
export const chordsRegEx = new RegExp(chordsRegExFields.pattern, chordsRegExFields.flags)
const chordLineRegEx = new RegExp(chordLinesRegExFields.pattern, chordLinesRegExFields.flags)

interface Replacement {
  index: number
  length: number
  replacement: string
}

interface Directive {
  title: string
  content: string
}

export type RenderMode = 'chordName' | 'diagram'

export class Renderer {
  chords: {
    [name: string]: Chord
  } = {}

  directives: Directive[] = []

  replacements: Replacement[] = []

  chordRenderMode: RenderMode

  text: string = ''

  constructor (text: string, defaultRenderMode: RenderMode = 'chordName') {
    this.text = text

    // If I don't add a linebreak in the end if the last thing is a chord it won't be parsed
    if (this.text[this.text.length - 1] !== '\n\n') this.text += '\n\n'
    this.chordRenderMode = defaultRenderMode

    this.parseDirectives()

    this.parseLyricsComments()

    this.parseChords()
  }

  get title (): string {
    return this.directive('title')
  }

  get artist (): string {
    return this.directive('artist')
  }

  directive (name: string): string {
    const titleDirective: Directive[] = this.directives.filter(directive => directive.title === name)
    return (titleDirective.length > 0) ? titleDirective[0].content : ''
  }

  parseChords (): void {
    const chordLines = Array.from(this.text.matchAll(chordLineRegEx))

    for (let i = 0; i < chordLines.length; i++) {
      const chordLine = chordLines[i]
      const lineIndex: number = chordLine.index ?? 0

      /* let's findout if the next line contains lyrics.
      If that is the case we'll join them in html section
      */
      const nextLineIndex = lineIndex + chordLine[0].length + 1
      const nextLineLength = this.text.substr(nextLineIndex).indexOf('\n') + 1
      const nextLineIsLyrics = ((): boolean => {
        if (i === chordLines.length - 1) {
          return nextLineIndex < this.text.length
        }
        const nextChordLine = chordLines[i + 1]
        const nextChordLineIndex = nextChordLine.index ?? 0
        if (nextChordLineIndex !== nextLineIndex && this.text[nextLineIndex] !== '\n') {
          return true
        }
        return false
      })()
      if (nextLineIsLyrics) {
        const lineSectionStart = '<section class="chords-line">'
        this.replacements.push({ index: lineIndex, length: 0, replacement: lineSectionStart })
        const lineSectionStop = '</section>'
        this.replacements.push({ index: nextLineIndex + nextLineLength - 1, length: 0, replacement: lineSectionStop })
      }

      const chordMatches = chordLine[0].matchAll(chordsRegEx)
      for (const match of chordMatches) {
        const index: number = lineIndex + (match.index ?? 0)
        const width = match[0].length - (match[7]?.length ?? 0) // I remove the comments length (if any comment at the end of the line)
        const chordName = match[1] ?? match[2]
        const chordRenderName = match[2]
        const chordVariant = match[3]
        const chordDiagram = match[4]
        const diagramOnly = match[5]

        let replacement: string

        if (diagramOnly !== undefined) { // a diagram alone
          const diagram = new Diagram(match[5])
          replacement = this.renderDiagram(diagram, width)
          this.replacements.push({ index, length: width, replacement })
        } else {
          const chord = chordName in this.chords ? this.chords[chordName] : new Chord(chordName)
          if (chordDiagram !== undefined) { // It's a chord definition
            const diagram = new Diagram(chordDiagram, chordRenderName, chordVariant)
            chord.setDiagram(diagram, chordVariant)
            this.replacements.push({ index, length: match[0].length, replacement: '' }) // remove it
          } else {
            replacement = this.renderChord(chord, width, chordVariant, this.chordRenderMode)
            this.replacements.push({ index, length: width, replacement })
          }
          this.chords[chordName] = chord
        }
      }
    }
  }

  parseDirectives (): number {
    const regEx = /{([\w-]+):\s*([\S\s]+?)}/g
    const matches = this.text.matchAll(regEx)
    let index: number = 0
    let width: number = 0

    for (const match of matches) {
      index = match.index ?? 0
      width = match[0].length
      const title: string = match[1]
      const content: string = match[2]
      let replacement = ''
      switch (title) {
        case 'columns':
          break
        case 'render-mode':
          if (content === 'diagram' || content === 'chordName') this.chordRenderMode = content
          break
        case 'lyrics-font-size':
          break
        case 'lyrics-font-color':
          break
        case 'chords-font-size':
          break
        case 'chords-font-color':
          break
        case 'comments-font-size':
          break
        case 'comments-font-color':
          break
        case 'show-chord-diagrams':
          break
        default:
          replacement = this.renderDirective({ title, content })
          break
      }
      this.directives.push({ title, content })
      this.replacements.push({ index, length: width, replacement })
    }
    // Create div for lyrics after directives
    while (this.text[index + width] === '\n') {
      width++
    }
    let divOpen = '<lyrics style="'
    const columns = Number(this.directive('columns'))
    if (isNaN(columns) || columns === 0) {
      divOpen += `columns: ${longestLineLength(this.text.substr(index + width)) + 2}ch`
    } else {
      divOpen += `column-count: ${columns}`
    }
    if (this.directive('lyrics-font-color') !== '') divOpen += `; color: ${this.directive('lyrics-font-color')}`
    divOpen += '">'
    const divClose = '</lyrics>'
    this.replacements.push({ index: index + width, length: 0, replacement: divOpen })
    this.replacements.push({ index: this.text.length - 1, length: 0, replacement: divClose })

    return index + width // where the lyrics should start
  }

  parseLyricsComments (): void {
    const regEx = /\s\/\/(.*)$/gm
    const matches = this.text.matchAll(regEx)

    let commentsFontSize = this.directive('comments-font-size')
    commentsFontSize = (commentsFontSize !== '') ? `font-size: ${commentsFontSize};` : ''
    let commentsFontColor = this.directive('comments-font-color')
    commentsFontColor = (commentsFontColor !== '') ? `color: ${commentsFontColor};` : ''
    for (const match of matches) {
      const index = match.index ?? 0
      const width = match[0].length
      const comment = match[1]
      const replacement = `<span class="comment" style="${commentsFontSize} ${commentsFontColor}">${comment}</span>`
      this.replacements.push({ index, length: width, replacement })
    }
  }

  private renderDirective (directive: Directive): string {
    return `<div class="directive"><label for="directive-${directive.title}">${directive.title}</label><div id="directive-${directive.title}" class="content">${directive.content}</div></div>`
  }

  private renderChord (chord: Chord, width: number, chordVariant?: string, renderMode?: RenderMode): string {
    renderMode = renderMode ?? 'chordName'

    const diagram = chord.getDiagram(chordVariant)
    const svgDiagram = (diagram !== undefined) ? new SvgDiagram(diagram) : undefined

    if (renderMode === 'diagram' && svgDiagram !== undefined) {
      return `<span class="chord-container" style="width: ${width}ch; height: 64px;">${svgDiagram.svg()}</span>`
    }

    const text = (chordVariant !== undefined) ? `${chord.name}:${chordVariant}` : chord.name
    let rendered = ''
    if (svgDiagram !== undefined) { rendered += `<span class="diagram" style="left: 100%">${svgDiagram.svg()}</span>` }

    let chordsFontSize = this.directive('chords-font-size')
    chordsFontSize = (chordsFontSize !== '') ? `font-size: ${chordsFontSize};` : ''
    let chordsFontColor = this.directive('chords-font-color')
    chordsFontColor = (chordsFontColor !== '') ? `color: ${chordsFontColor};` : ''

    rendered = `<span class="chord-name" style="${chordsFontSize} ${chordsFontColor}">${text}${rendered}</span>`

    return `<span class="chord-container" style="width: ${width}ch">${rendered}</span>`
  }

  private renderDiagram (diagram: Diagram, width: number, unit: string = 'ch', showVariant?: boolean): string {
    const svgDiagram = new SvgDiagram(diagram)

    return `<span class="chord-container" style="width: ${width}${unit}; height: 64px">${svgDiagram.svg(showVariant)}</span>`
  }

  renderDiagrams (): string {
    function getChordRoot (chordName: string): string {
      let chordRoot = ''
      if (chordName.length > 1 && (chordName[1] === '#' || chordName[1] === 'b')) {
        chordRoot = `${chordName[0]}${chordName[1]}`
      } else {
        chordRoot = chordName[0]
      }
      return chordRoot
    }
    let ret = ''

    const sortedChordsNames = Object.keys(this.chords).sort((a, b) => {
      const aRoot = getChordRoot(a)
      const bRoot = getChordRoot(b)

      return ((aRoot.length - bRoot.length) !== 0 && a[0] === b[0]) ? aRoot.length - bRoot.length : a.localeCompare(b)
    })

    let root = ''
    for (const chordName of sortedChordsNames) {
      const chord = this.chords[chordName]
      const chordRoot = getChordRoot(chordName)
      if (chordRoot !== root) {
        ret += (root === '') ? '<p>' : '</p><p>'
        root = chordRoot
      }

      // print default diagram
      const defaultDiagram = chord.getDiagram()
      if (defaultDiagram === undefined) {
        console.warn(`A diagram for ${chordName} hasn't be provided`)
        ret += chordName
      } else {
        ret += this.renderDiagram(chord.getDiagram(), 64, 'px', true)
      }
      // and now the rest of variants
      for (const chordVariant in chord.diagrams) {
        ret += this.renderDiagram(chord.getDiagram(chordVariant), 64, 'px', true)
      }
    }
    ret += '</p>'
    return `<chords><div class="title">Chords in this song:</div>${ret}</chords>`
  }

  render (): string {
    let ret: string = ''
    let index = 0
    const replacements = this.replacements.sort((a, b) => a.index - b.index)
    for (const replacement of replacements) {
      ret += this.text.slice(index, replacement.index)
      ret += replacement.replacement
      index = replacement.index + replacement.length
    }
    ret += this.text.slice(index)

    // remove any preceeding linebreak before starting the lyrics
    ret = ret.replace(/(<lyrics[\s\S]*?>)(\n*)/, '$1')

    if (this.directive('show-chord-diagrams') === 'true') ret += this.renderDiagrams()
    let fontSizeStyle = ''
    if (this.directive('lyrics-font-size') !== '') fontSizeStyle += ` style="font-size: ${this.directive('lyrics-font-size')}"`
    return `${SvgDiagram.svgDefs}<chordsong${fontSizeStyle}>${ret}</chordsong>`
  }
}

function longestLineLength (str: string): number {
  const strArr = str.split('\n')
  let max = strArr[0].length
  strArr.forEach(v => {
    max = Math.max(max, v.length)
  })
  return max
}
