export interface FretStrings {
  [fret: string]: number[] //  is an array of pressed strings per fret
}

export interface FingerOrBarre {
  finger?: number // The finger that presses the fret string/s
  fret: number
  stringFrom: number
  stringTo: number
}
export const stringsFretsRegEx = /^([oOxX\d]{6}|(?:(?:[xXoO]|\d{1,2}),){5}(?:[xXoO]|\d{1,2}))$/
export const fretStringsRegEx = /^(([12]?\d):([1-6](-[1-6])?)(,[1-6](-[1-6])?){0,5})(\/([12]?\d):([1-6](-[1-6])?)(,[1-6](-[1-6])?){0,5}){1,3}$/

export class Diagram {
  chordName?: string
  chordVariant?: string
  chordRenderName?: string
  private _stringFrets: number[] = [] // array of pressed frets by string, starting by the thickest string.
  readonly fretStrings: FretStrings = {} // list of strings pressed at a given fret.
  private _fingersAndBarres: FingerOrBarre[] = [] // array with pressed strings in every fret by each finger

  constructor (parsedDiagram: string, chordName?: string, chordVariant?: string, chordRenderName?: string) {
    this.chordName = chordName
    this.chordVariant = chordVariant
    this.chordRenderName = chordRenderName ?? chordName
    try {
      this.fromStringFrets(parsedDiagram)
    } catch (error) {
      this.fromFingersAndBarres(parsedDiagram)
    }
  }

  get stringFrets (): number[] {
    return this._stringFrets
  }

  set stringFrets (arr: number[]) {
    this._stringFrets = arr
    this._computeFretStrings()
    this._computeFingersAndBarrels()
  }

  get fingersAndBarrels (): FingerOrBarre[] {
    return this._fingersAndBarres
  }

  set fingersAndBarrels (arr: FingerOrBarre[]) {
    this._fingersAndBarres = arr
    this._computeStringFrets()
    this._computeFretStrings()
  }

  get minFret (): number {
    return Math.min(...this.stringFrets.filter(fret => fret > 0))
  }

  get maxFret (): number {
    return Math.max(...this.stringFrets)
  }

  private fromStringFrets (parsedDiagram: string): void {
    if (!stringsFretsRegEx.test(parsedDiagram)) {
      throw new Error(`invalid stringFrets format for ${parsedDiagram}`)
    }
    parsedDiagram = parsedDiagram
      .replace(/\|/g, '')
      .replace(/[Oo]/g, '0')
      .replace(/[Xx]/g, 'x')

    // Let's take what fret is assigned to every string.
    // When stringFrets is set, fretString and fingersAndBarrels are updated
    this.stringFrets = (parsedDiagram.match(/[x0-9]{6}/) != null
      ? parsedDiagram.split('')
      : parsedDiagram.split(',')
    ).map((str: string) => str === 'x' ? -1 : Number(str))
  }

  private fromFingersAndBarres (parsedDiagram: string): void {
    // Expected input is a semicolon-sepparated string of fret/[stringRange,...]
    // Use fret 0 for open strings. Strings that are neither pressed or open are assumed to be muted
    // stringRange can be a single number from 1 (thinest) to 6 (thickest) or a range, eg.: 2 or 1-6
    // Examples:
    //  - barre F (132211):  1:6-1/2:3/3:4,5
    //  - E (022100): 0:1,2,6/1:3/2:4,5
    //  - Cm (x35543): 3:1-5/4:2/5:3,4
    //  - A (x02220): 0:1,5/2:3-4,2
    if (!fretStringsRegEx.test(parsedDiagram)) {
      throw new Error(`invalid format for ${parsedDiagram}`)
    }
    const fingersAndBarres: FingerOrBarre[] = []
    parsedDiagram.split('/').forEach(item => {
      const parsed: FingerOrBarre[] = []
      let fret: number
      item.split(':').forEach((value, index) => {
        if (index === 0) {
          fret = Number(value)
        } else if (index === 1) {
          value.split(',').forEach(range => {
            let a = Number(range[0])
            a = 6 - a // the 6th string is our 0, the 5th our one, and so on
            let b = Number(range[range.length - 1])
            b = 6 - b // the 6th string is our 0, the 5th our one, and so on
            parsed.push({
              fret,
              stringFrom: (a < b) ? a : b,
              stringTo: (a >= b) ? a : b
            })
          })
        } else {
          throw new Error(`Invalid diagram ${parsedDiagram}`)
        }
      })
      parsed.forEach(item => {
        fingersAndBarres.push(item)
      })
    })

    this.fingersAndBarrels = fingersAndBarres
    // After fingersAndBarrels is set, stringFrets and fretString are automatically updated, so no need to do it manually
  }

  private _computeBarrel (): FingerOrBarre | null {
    // barrel not needed if:
    //  1. We need 4 or less fingers
    //  2. We need 5 fingers but there is a fret in the 6th string (it could be pressed with the thumb).
    const fingeredFrets = this.stringFrets.filter((item) => item > 0)
    if (fingeredFrets.length < 5 || (fingeredFrets.length === 5 && this.stringFrets[0] > 0)) {
      return null
    }

    const barrels: {
      [fretPos: string]: {
        from: number
        to: number
      }
    } = {}

    for (const [fretPos, strings] of Object.entries(this.fretStrings)) {
      if (Number(fretPos) <= 0) continue

      const from = Math.min(...strings)
      let to = from
      for (let i = from + 1; i < 6; i++) {
        if (!strings.includes(i) && this.stringFrets[i] >= 0 && this.stringFrets[i] < Number(fretPos)) {
          break
        } else {
          to++
        }
      }
      barrels[fretPos] = { from, to }
    }
    let fret = 0
    let stringFrom = 0
    let stringTo = 0
    for (const [fretPos, fromTo] of Object.entries(barrels)) {
      if (fromTo.to - fromTo.from + 1 > stringTo - stringFrom) {
        fret = Number(fretPos)
        stringFrom = fromTo.from
        stringTo = fromTo.to
      }
    }
    return { fret, stringFrom, stringTo }
  }

  private _computeStringFrets (): void {
    this._stringFrets = [-1, -1, -1, -1, -1, -1]
    this._fingersAndBarres.forEach(item => {
      const fret = item.fret
      const strings: number[] = range(item.stringTo, item.stringFrom)
      strings.forEach(item => {
        if (fret > this._stringFrets[item]) {
          this._stringFrets[item] = fret
        }
      })
    })
  }

  private _computeFretStrings (): void {
    this._stringFrets.forEach((fret, string) => {
      if (String(fret) in this.fretStrings) {
        this.fretStrings[String(fret)].push(string)
      } else {
        this.fretStrings[String(fret)] = [string]
      }
    })
  }

  private _computeFingersAndBarrels (): void {
    const barrel = this._computeBarrel()
    if (barrel != null) {
      this._fingersAndBarres.push(barrel)
    }
    for (const [fretPos, strings] of Object.entries(this.fretStrings)) {
      if (Number(fretPos) <= 0) continue
      if (barrel === null || Number(fretPos) !== barrel.fret) {
        strings.forEach((fretStr) =>
          this._fingersAndBarres.push({
            fret: Number(fretPos),
            stringFrom: fretStr,
            stringTo: fretStr
          })
        )
      }
    }
  }

  toString (): string {
    return this.stringFrets.join(',').replace(/-1/g, 'x').replace(/0/g, 'o')
  }
}

function range (max: number, min: number = 0, step: number = 1): number[] {
  const arr: number[] = []
  for (let i = min; i <= max; i += step) {
    arr.push(i)
  }
  return arr
}
