import type { PluginWithOptions } from 'markdown-it'
import Renderer from 'markdown-it/lib/renderer'
import type Token from 'markdown-it/lib/token'

type CitationMode = 'AuthorInText' | 'SuppressAuthor' | 'NormalCitation'

export interface Citation {
  citationId: string,
  citationPrefix: Token[],
  citationSuffix: Token[],
  citationMode: CitationMode,
  citationNoteNum: number,
  citationHash: number
}

interface CiteProc<T> {    
  appendCluster(cluster: Citation[]): T
  renderCluster(id: T, renderer: Renderer): string
  renderBibliography(): string
}

interface CitationOptions {
  citeproc: (env: object) => CiteProc<any>
  "suppress-bibliography"?: boolean
  "reference-section-title"?: string
}

const Citations: PluginWithOptions<CitationOptions> = (md,options) => {
  const regexes = {
    citation: /^([^^-]|[^^].+?)?(-)?@([\w][\w:.#$%&\-+?<>~\/]*)(.+)?$/,
    inText: /^@([\w][\w:.#$%&\-+?<>~\/]*)(\s*)(\[)?/,
    allowedBefore: /^[^a-zA-Z.0-9]$/
  }  

  md.inline.ruler.after('emphasis', 'citation', (state,silent) => {
    let max = state.posMax
    const char = state.src.charCodeAt(state.pos)
    if (char == 0x40 /* @ */ && 
      (state.pos == 0 || regexes.allowedBefore.test(state.src.slice(state.pos - 1, state.pos)))) { // in-text      
      const match = state.src.slice(state.pos).match(regexes.inText)
      if (match) {        
        const citeproc: CiteProc<any> | undefined = options?.citeproc ? state.env.citeproc || (state.env.citeproc = options?.citeproc(state.env)) : undefined
        let citation: Citation = {
          citationId: match[1],
          citationPrefix: [],
          citationSuffix: [],
          citationMode: 'AuthorInText',
          citationNoteNum: (state.env.noteNum = (state.env.noteNum || 0) + 1),
          citationHash: 0          
        }
        let token: Token | undefined
        if (!silent) {
          token = state.push('cite_open', 'span', 1)
          token.attrSet('class','citation')
          token.attrSet('data-cites', match[1])
        }
        if (match[3]) { // suffix is there
          const suffixStart = state.pos + match[0].length
          const suffixEnd = state.md.helpers.parseLinkLabel(state, suffixStart)
          const charAfter = state.src.codePointAt(suffixEnd + 1)
          if (suffixEnd > 0 && charAfter != 0x28 && charAfter != 0x5B /* ( or [ */) {
            const suffix = state.src.slice(suffixStart, suffixEnd)
            citation.citationSuffix = state.md.parseInline(suffix,state.env)
            state.pending += match[0] + suffix + ']'
            state.pos += match[0].length + suffixEnd - suffixStart + 1
          } else {
            state.pending += '@' + match[1]
            state.pos += match[0].length - match[2].length - match[3].length
          }
        } else {
          state.pos += match[0].length - match[2].length
        }
        if (token) {
          token.meta = citeproc?.appendCluster([citation])     
          state.pushPending()
          state.push('cite_close', 'span', -1)
        }
        return true
      }
    } else if (char == 0x5B /* [ */) {
      const end = state.md.helpers.parseLinkLabel(state, state.pos)
      const charAfter = state.src.codePointAt(end + 1)
      if (end > 0 && charAfter != 0x28 && charAfter != 0x5B) {
        const str = state.src.slice(state.pos + 1,end)
        const parts = str.split(';').map(x => x.match(regexes.citation))
        if (parts.indexOf(null) >= 0) {
          return false
        } else {
          const citeproc: CiteProc<any> | undefined = options?.citeproc ? state.env.citeproc || (state.env.citeproc = options?.citeproc(state.env)) : undefined
          const cites: Citation[] = (parts as RegExpMatchArray[]).map(x => ({
            citationId: x[3],
            citationPrefix: x[1] ? state.md.parseInline(x[1], state.env) : [],
            citationSuffix: x[4] ? state.md.parseInline(x[4], state.env) : [],
            citationMode: x[2] ? 'SuppressAuthor' : 'NormalCitation',
            citationNoteNum: (state.env.noteNum = (state.env.noteNum || 0) + 1),
            citationHash: 0            
          }))          
          if (!silent) {            
            const token = state.push('cite_open','span',1);
            token.meta = citeproc?.appendCluster(cites)
            token.attrSet('class','citation')
            token.attrSet('data-cites',cites.map(x => x.citationId).join(' '))
            state.pending = state.src.slice(state.pos,end + 1);
            state.push('cite_close','span',-1);
          }
          state.pos = end + 1
          return true
        }        
      }      
      return false
    }
    return false
  })  

  if (options?.citeproc) {    
    md.renderer.rules['cite_open'] = (tkns,idx,opts,env,slf) => {
      const citeproc = env.citeproc || options.citeproc(env)
      tkns[idx+1].content = ''
      return citeproc.renderCluster(tkns[idx].meta,slf)
    }
    md.renderer.rules['cite_close'] = () => ''  

    if (!options?.['suppress-bibliography']) {
      md.core.ruler.push('bibliography',(state) => {    
        if (!state.inlineMode) {
          const i = state.tokens.findIndex((tk) => tk.attrGet('id') == 'refs')          
          if (i >= 0) {
            const refsOpen = state.tokens[i]
            if (refsOpen.nesting != 1) {
              return false
            }
            let j = i + 1
            while (j < state.tokens.length) {
              if (state.tokens[j].tag == refsOpen.tag && state.tokens[j].level == refsOpen.level && state.tokens[j].nesting == -1) {
                state.tokens.splice(j,0,new state.Token('bibliography','',0))
                return true
              }
              j += 1
            } 
            return false
          } else {
            const refsOpen = new state.Token('refs_container','div',1)            
            refsOpen.attrSet('id','refs')
            state.tokens.push(refsOpen)
            state.tokens.push(new state.Token('bibliography','',0))
            state.tokens.push(new state.Token('refs_container','div',-1))
            return true
          }                     
        } else return false
      })
    
      md.renderer.rules['bibliography'] = (tks,idx,opts,env) => {      
        const citeproc: CiteProc<any> | undefined = options?.citeproc ? env.citeproc || (env.citeproc = options?.citeproc(env)) : undefined
        return citeproc?.renderBibliography() || '<NO CITEPROC CONFIGURED>'
      }
    }
  }
}

export default Citations