/**  UI code for individual messages: display them, edit them
 *
 * @packageDocumentation
 */

/* global $rdf */
import { insertMessageIntoTable } from './infinite'
import { messageToolbar, sentimentStripLinked } from './messageTools'
import { findBookmarkDocument } from './bookmarks'
import { mostRecentVersion, originalVersion, allVersions } from './chatLogic'
import * as debug from '../debug'
import { icons } from '../iconBase'
import { store, authn } from 'solid-logic'
import { ensureLoggedIn } from '../login/login'
import { media } from '../media/index'
import ns from '../ns'
import * as pad from '../pad'
import { style } from '../style'
import * as utils from '../utils'
import * as widgets from '../widgets'
import { getBlankMsg, verifySignature, SEC } from './signature'
import { getPublicKey } from './keys'

const dom = window.document
const messageBodyStyle = style.messageBodyStyle

const label = utils.label

/**
 * elementForImageURI
 * HTML component for an image
 * @param imageUri
 * @param options { inlineImageHeightEms }
 * @returns HTMLAnchorElement For Image
 */
export function elementForImageURI (imageUri, options) {
  const img = dom.createElement('img')
  let height = '10'
  if (options.inlineImageHeightEms) {
    height = ('' + options.inlineImageHeightEms).trim()
  }
  img.setAttribute(
    'style',
    'max-height: ' + height + 'em; border-radius: 1em; margin: 0.7em;'
  )
  // widgets.makeDropTarget(img, handleURIsDroppedOnMugshot, droppedFileHandler)
  if (imageUri) img.setAttribute('src', imageUri)
  const anchor = dom.createElement('a')
  anchor.setAttribute('href', imageUri)
  anchor.setAttribute('target', 'images')
  anchor.appendChild(img)
  widgets.makeDraggable(img, $rdf.sym(imageUri))
  return anchor
}

const anchor = function (text, term) {
  // If there is no link return an element anyway
  const a = dom.createElement('a')
  if (term && term.uri) {
    a.setAttribute('href', term.uri)
    a.addEventListener('click', widgets.openHrefInOutlineMode, true)
    a.setAttribute('style', 'color: #3B5998; text-decoration: none; ') // font-weight: bold
  }
  a.textContent = text
  return a
}

function nickname (person) {
  const s = store.any(person, ns.foaf('nick'))
  if (s) return '' + s.value
  return '' + label(person)
}

/**
 * creatorAndDate
 * Displays creator and date for a chat message
 * inside the `td1` element
 * @param td1
 * @param creator
 * @param date
 * @param message
 * @returns HTMLAnchorElement For Image
 */
export function creatorAndDate (td1, creator, date, message) {
  const nickAnchor = td1.appendChild(anchor(nickname(creator), creator))
  if (creator.uri) {
    store.fetcher.nowOrWhenFetched(creator.doc(), undefined, function (
      _ok,
      _body
    ) {
      nickAnchor.textContent = nickname(creator)
    })
  }
  td1.appendChild(dom.createElement('br'))
  td1.appendChild(anchor(date, message))
}

/**
 * creatorAndDateHorizontal
 * Horizontally displays creator and date for a chat message
 * inside the `td1` element
 * @param td1
 * @param creator
 * @param date
 * @param message
 * @returns HTMLAnchorElement For Image
 */
export function creatorAndDateHorizontal (td1, creator, date, message) {
  const nickAnchor = td1.appendChild(anchor(label(creator), creator))
  if (creator.uri) {
    store.fetcher.nowOrWhenFetched(creator.doc(), undefined, function (
      _ok,
      _body
    ) {
      nickAnchor.textContent = nickname(creator)
    })
  }
  const dateBit = td1.appendChild(anchor(date, message))
  dateBit.style.fontSize = '80%'
  dateBit.style.marginLeft = '1em'
  td1.appendChild(dom.createElement('br'))
}

/**
 * renderMessageRow
 * Renders a chat message, read-only mode
 * @param channelObject
 * @param message
 * @param fresh
 * @param options
 * @param userContext
 * @returns Message Row HTML Table Element
 */
export async function renderMessageRow (channelObject, message, fresh, options, userContext) {
  let unsignedMessage = false
  const colorizeByAuthor =
    options.colorizeByAuthor === '1' || options.colorizeByAuthor === true

  // const id = store.any(latestVersion, ns.sioc('id'))
  // const replies = store.each(latestVersion, ns.sioc('has_reply'))

  const creator = store.any(message, ns.foaf('maker'))
  const date = store.any(message, ns.dct('created'))
  const latestVersion = await mostRecentVersion(message)
  const latestVersionCreator = store.any(latestVersion, ns.foaf('maker'))

  // use latest content if same owner, else use original
  // this is may be too strict. Should we find latest valid version if any ?
  const msgId = creator.uri === latestVersionCreator?.uri ? latestVersion : message
  const content = store.any(msgId, ns.sioc('content'))

  const versions = await allVersions(msgId)
  if (versions.length > 1) {
    debug.log('renderMessageRow versions: ', versions.join(',  '))
  }
  // be tolerant in accepting replies on any version of a message
  const replies = versions.map(version => store.each(version, ns.sioc('has_reply'))).flat()

  let thread = null
  const straightReplies = []
  for (const reply of replies) {
    if (store.holds(reply, ns.rdf('type'), ns.sioc('Thread'))) {
      thread = reply
      debug.log('renderMessageRow: found thread: ' + thread)
    } else {
      straightReplies.push(reply)
    }
  }
  if (straightReplies.length > 1) {
    debug.log('renderMessageRow: found normal replies: ', straightReplies)
  }
  if (!thread) {
    // thread = store.any(message, ns.sioc('has_reply'))
    thread = store.any(null, ns.sioc('has_member'), message)
  }
  // debug.log('@@@@ is thread' + thread)

  // get signature
  const signature = store.any(msgId, $rdf.sym(`${SEC}proofValue`))

  // set proof message object
  const msg = getBlankMsg()
  msg.id = msgId.uri
  msg.created = store.any(msgId, ns.dct('created')).value
  msg.content = content.value
  msg.maker = creator.uri

  // verify signature
  if (!signature?.value) { // unsigned message
    unsignedMessage = true
    debug.warn(msgId.uri + ' is unsigned') // TODO replace with UI (colored message ?)
  } else { // signed message, get public key and check signature
    getPublicKey(creator).then(publicKey => {
      // debug.log(creator.uri + '\n' + msg.created + '\n' + msg.id + '\n' + publicKey)
      if (!publicKey) {
        debug.warn('message is signed but ' + creator.uri + ' is missing publicKey')
      }
      // check that publicKey is a valid hex string
      const regex = /[0-9A-Fa-f]{6}/g
      if (!publicKey?.match(regex)) debug.warn('invalid publicKey hex string\n' + creator.uri + '\n' + publicKey)
      // verify signature
      else if (signature?.value && !verifySignature(signature?.value, msg, publicKey)) debug.warn('invalid signature\n' + msg.id)
    })
  }

  const originalMessage = await originalVersion(message)
  const edited = !message.sameTerm(originalMessage)
  // @@ load it first  @@   Or display the new data at the old date.
  // @@@ kludge!
  const sortDate = store.the(originalMessage, ns.dct('created'), null, originalMessage.doc()) || store.the(message, ns.dct('created'), null, message.doc())
  // In message

  const messageRow = dom.createElement('tr')
  if (unsignedMessage) messageRow.setAttribute('style', 'background-color: red')
  messageRow.AJAR_date = sortDate.value
  messageRow.AJAR_subject = message

  const td1 = dom.createElement('td')
  messageRow.appendChild(td1)
  if (!options.authorDateOnLeft) {
    const img = dom.createElement('img')
    img.setAttribute(
      'style',
      'max-height: 2.5em; max-width: 2.5em; border-radius: 0.5em; margin: auto;'
    )
    widgets.setImage(img, creator)
    td1.appendChild(img)
  } else {
    creatorAndDate(td1, creator, widgets.shortDate(sortDate.value), message)
  }
  let bothDates = widgets.shortDate(sortDate.value)
  if (edited) {
    bothDates += ' ... ' + widgets.shortDate(date.value)
  }

  // Render the content ot the message itself
  const td2 = messageRow.appendChild(dom.createElement('td'))

  if (!options.authorDateOnLeft) {
    creatorAndDateHorizontal(
      td2,
      creator,
      bothDates, // widgets.shortDate(dateString)
      message
    )
  }
  const text = content ? content.value.trim() : '??? no content?'
  const isURI = /^https?:\/[^ <>]*$/i.test(text)
  let para = null
  if (isURI) {
    const isImage = /\.(gif|jpg|jpeg|tiff|png|svg)$/i.test(text) // @@ Should use content-type not URI
    if (isImage && options.expandImagesInline) {
      const img = elementForImageURI(text, options)
      td2.appendChild(img)
    } else {
      // Link but not Image
      const anc = td2.appendChild(dom.createElement('a'))
      para = anc.appendChild(dom.createElement('p'))
      anc.href = text
      para.textContent = text
      td2.appendChild(anc)
    }
  } else {
    // text
    para = dom.createElement('p')
    td2.appendChild(para)
    para.textContent = text
  }
  if (para) {
    const bgcolor = colorizeByAuthor
      ? pad.lightColorHash(creator)
      : getBgColor(fresh)
    para.setAttribute(
      'style',
      messageBodyStyle + 'background-color: ' + bgcolor + ';'
    )
  }

  function getBgColor (fresh) {
    return fresh ? '#e8ffe8' : 'white'
  }

  // Sentiment strip
  const strip = await sentimentStripLinked(message, message.doc())
  if (strip.children.length) {
    td2.appendChild(dom.createElement('br'))
    td2.appendChild(strip)
  }

  // Message tool bar button
  const td3 = dom.createElement('td')
  messageRow.appendChild(td3)
  const toolsButton = widgets.button(
    dom,
    icons.iconBase + 'noun_243787.svg',
    '...'
  )
  td3.appendChild(toolsButton)
  toolsButton.addEventListener('click', async function (_event) {
    if (messageRow.toolTR) {
      // already got a toolbar? Toogle
      messageRow.parentNode.removeChild(messageRow.toolTR)
      delete messageRow.toolTR
      return
    }
    const toolsTR = dom.createElement('tr')
    const tools = await messageToolbar(message, messageRow, { ...userContext, chatOptions: options }, channelObject)
    tools.style =
      'border: 0.05em solid #888; border-radius: 0 0 0.7em 0.7em;  border-top: 0; height:3.5em; background-color: #fff;' // @@ fix
    if (messageRow.nextSibling) {
      messageRow.parentElement.insertBefore(toolsTR, messageRow.nextSibling)
    } else {
      messageRow.parentElement.appendChild(toolsTR)
    }
    messageRow.toolTR = toolsTR
    toolsTR.appendChild(dom.createElement('td')) // left
    const toolsTD = toolsTR.appendChild(dom.createElement('td'))
    toolsTR.appendChild(dom.createElement('td')) // right
    toolsTD.appendChild(tools)
  })
  if (thread && options.showThread) {
    // debug.log('  message has thread ' + thread)
    td3.appendChild(widgets.button(
      dom,
      icons.iconBase + 'noun_1180164.svg', // right arrow .. @@ think of stg better
      'see thread',
      _e => {
        // debug.log('@@@@ Calling showThread thread ' + thread)
        options.showThread(thread, options)
      }
    ))
  }
  return messageRow
} // END OF RENDERMESSAGE

export async function switchToEditor (messageRow, message, channelObject, userContext) {
  const messageTable = messageRow.parentNode
  const editRow = renderMessageEditor(channelObject, messageTable, userContext,
    channelObject.options, await mostRecentVersion(message))
  messageTable.insertBefore(editRow, messageRow)
  editRow.originalRow = messageRow
  messageRow.style.visibility = 'hidden' // Hide the original message. unhide if user cancels edit
}
/*       Control for a new message -- or editing an old message ***************
 *
 */
export function renderMessageEditor (channelObject, messageTable, userContext, options, originalMessage) {
  function revertEditing (messageEditor) {
    messageEditor.originalRow.style.visibility = 'visible' // restore read-only version
    messageEditor.parentNode.removeChild(messageEditor)
  }

  async function handleFieldInput (_event) {
    await sendMessage(field.value, true)
  }

  async function sendMessage (text, fromMainField) {
    async function sendComplete (message, _text2) {
      // const dateStamp = store.any(message, ns.dct('created'), null, message.doc())
      // const content = $rdf.literal(text2)
      await insertMessageIntoTable(channelObject, messageTable, message, false, options, userContext) // not green

      if (originalMessage) { // editing another message
        const oldRow = messageEditor.originalRow
        // oldRow.style.display = '' // restore read-only version, re-attack
        if (oldRow.parentNode) {
          oldRow.parentNode.removeChild(oldRow) // No longer needed old version
        } else {
          debug.warn('No parentNode on old message ' + oldRow.textContent)
          oldRow.style.backgroundColor = '#fee'
          oldRow.style.visibility = 'hidden' // @@ FIX THIS AND REMOVE FROM DOM INSTEAD
        }
        messageEditor.parentNode.removeChild(messageEditor) // no longer need editor
      } else {
        if (fromMainField) {
          field.value = '' // clear from out for reuse
          field.setAttribute('style', messageBodyStyle)
          field.disabled = false
          field.scrollIntoView(options.newestFirst) // allign bottom (top)
          field.focus() // Start typing next line immediately
          field.select()
        }
      }
      // await channelObject.div.refresh() // Add new day if nec  @@ add back
    }

    // const me = authn.currentUser() // Must be logged on or wuld have got login button
    if (fromMainField) {
      field.setAttribute('style', messageBodyStyle + 'color: #bbb;') // pendingedit
      field.disabled = true
    }

    let message
    try {
      message = await channelObject.updateMessage(text, originalMessage, null, options.thread)
    } catch (err) {
      const statusArea = userContext.statusArea || messageEditor
      statusArea.appendChild(
        widgets.errorMessageBlock(dom, 'Error writing message: ' + err)
      )
      return
    }
    await sendComplete(message, text)
  } // sendMessage

  //    DRAG AND DROP
  function droppedFileHandler (files) {
    const base = messageTable.chatDocument.dir().uri
    widgets.uploadFiles(
      store.fetcher,
      files,
      base + 'Files',
      base + 'Pictures',
      async function (theFile, destURI) {
        // @@@@@@ Wait for each if several
        await sendMessage(destURI)
      }
    )
  }

  // When a set of URIs are dropped on the field
  const droppedURIHandler = async function (uris) {
    for (const uri of uris) {
      await sendMessage(uri)
    }
  }

  // When we are actually logged on
  function turnOnInput () {
    function getImageDoc () {
      imageDoc = $rdf.sym(
        chatDocument.dir().uri + 'Image_' + Date.now() + '.png'
      )
      return imageDoc
    }

    async function tookPicture (imageDoc) {
      if (imageDoc) {
        await sendMessage(imageDoc.uri)
      }
    }

    // Body of turnOnInput

    let menuButton
    if (options.menuHandler) {
      const menuButton = widgets.button(
        dom, icons.iconBase + 'noun_243787.svg', 'More')
      menuButton.setAttribute('style', style.buttonStyle + 'float: right;')
      // menuButton.addEventListener('click', _event => sendMessage(), false) (done in turnoninput)
      rhs.appendChild(menuButton)
    }

    if (options.menuHandler && menuButton) {
      const me = authn.currentUser()
      const menuOptions = {
        me,
        dom,
        div: null, // @@ was: div
        newBase: messageTable.chatDocument.dir().uri
      }
      menuButton.addEventListener(
        'click',
        event => {
          options.menuHandler(event, channelObject.chatChannel, menuOptions)
        },
        false
      )
    }

    const me = authn.currentUser() // If already logged on
    creatorAndDate(lhs, me, '', null)

    field = dom.createElement('textarea')
    middle.innerHTML = ''
    middle.appendChild(field)
    field.rows = 3
    if (originalMessage) {
      field.value = store.anyValue(originalMessage, ns.sioc('content'), null, originalMessage.doc())
    }
    // field.cols = 40
    field.setAttribute('style', messageBodyStyle + 'background-color: #eef;')

    // Trap the Enter BEFORE it is used ti make a newline

    field.addEventListener(
      'keydown',
      async function (e) {
        // User preference?
        if (e.code === 'Enter') {
        // if (e.keyCode === 13) { // deprocated https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyCode
          if ((!e.shiftKey && !options.shiftEnterSendsMessage) || (e.shiftKey && options.shiftEnterSendsMessage)) {
            // Shift-Enter just adds a new line
            // (note alt-enter doesn't add newline anyway on my setup - so have to use shift.
            await handleFieldInput(e)
          }
        }
      },
      false
    )

    widgets.makeDropTarget(field, droppedURIHandler, droppedFileHandler)

    rhs.innerHTML = ''

    sendButton = widgets.button(dom, sendIcon, 'Send')
    sendButton.style.float = 'right'
    sendButton.addEventListener('click', _event => handleFieldInput(), false)
    rhs.appendChild(sendButton)

    if (originalMessage) { // Are we editing another message?
      const cancelButton = rhs.appendChild(widgets.cancelButton(dom))
      cancelButton.style.float = 'left'
      // cancelButton.firstChild.style.opacity = '0.3' // moved to buttons
      cancelButton.addEventListener('click', _event => revertEditing(messageEditor), false)
      rhs.appendChild(cancelButton)
    }

    const chatDocument = channelObject.dateFolder.leafDocumentFromDate(new Date())
    let imageDoc

    middle.appendChild(
      media.cameraButton(dom, store, getImageDoc, tookPicture)
    )

    pad.recordParticipation(channelObject.channel, channelObject.channel.doc()) // participation =
  } // turn on inpuut

  // Body of renderMessageEditor

  let sortDate, sendIcon
  if (originalMessage) {
    sortDate = store.anyValue(originalMessage, ns.dct('created'), null, originalMessage.doc())
    // text = store.anyValue(originalMessage, ns.sioc('content'), null, originalMessage.doc())
    sendIcon = icons.iconBase + 'noun_1180158.svg' // Green check
    // cancelIcon = icons.iconBase + 'noun_1180156.svg' // Black cross
  } else {
    sendIcon = icons.iconBase + 'noun_383448.svg'
    sortDate = '9999-01-01T00:00:00Z' // ISO format for field sort
    // text = ''
  }
  const messageEditor = dom.createElement('tr')
  const lhs = dom.createElement('td')
  const middle = dom.createElement('td')
  const rhs = dom.createElement('td')
  messageEditor.appendChild(lhs)
  messageEditor.appendChild(middle)
  messageEditor.appendChild(rhs)
  messageEditor.AJAR_date = sortDate
  // messageEditor.appendChild(dom.createElement('br'))

  let field, sendButton
  const context = { div: middle, dom }

  ensureLoggedIn(context).then(context => {
    // me = context.me
    turnOnInput()
    Object.assign(context, userContext)
    findBookmarkDocument(context).then(_context => {
      // debug.log('Bookmark file: ' + context.bookmarkDocument)
    })
  })

  return messageEditor
} // renderMessageEditor
