require('cross-fetch/polyfill')
const ApolloClient = require('apollo-boost').ApolloClient;
const InMemoryCache = require('apollo-cache-inmemory').InMemoryCache;
const createHttpLink = require('apollo-link-http').createHttpLink;
const gql = require('graphql-tag')

// TEST_MODE can be turned on or off to prevent communications with the Apollo iMessage Server running on your modern Mac
const TEST_MODE = false
const DEBUG = false
let lastMessageFromSerialPortTime

const defaultOptions = {
  watchQuery: {
    fetchPolicy: 'no-cache',
    errorPolicy: 'ignore',
  },
  query: {
    fetchPolicy: 'no-cache',
    errorPolicy: 'ignore',
  },
}

let client

const widthFor12ptFont = [
  0,
  10,
  10,
  10,
  10,
  10,
  10,
  10,
  10,
  8,
  10,
  10,
  10,
  0,
  10,
  10,
  10,
  11,
  11,
  9,
  11,
  10,
  10,
  10,
  10,
  10,
  10,
  10,
  10,
  10,
  10,
  10,
  4,
  6,
  7,
  10,
  7,
  11,
  10,
  3,
  5,
  5,
  7,
  7,
  4,
  7,
  4,
  7,
  8,
  8,
  8,
  8,
  8,
  8,
  8,
  8,
  8,
  8,
  4,
  4,
  6,
  8,
  6,
  8,
  11,
  8,
  8,
  8,
  8,
  7,
  7,
  8,
  8,
  6,
  7,
  9,
  7,
  12,
  9,
  8,
  8,
  8,
  8,
  7,
  6,
  8,
  8,
  12,
  8,
  8,
  8,
  5,
  7,
  5,
  8,
  8,
  6,
  8,
  8,
  7,
  8,
  8,
  6,
  8,
  8,
  4,
  6,
  8,
  4,
  12,
  8,
  8,
  8,
  8,
  6,
  7,
  6,
  8,
  8,
  12,
  8,
  8,
  8,
  5,
  5,
  5,
  8,
  8
]

// this is tied to mac_main.c's message window max width
const MAX_WIDTH = 304
const SPACE_WIDTH = widthFor12ptFont[32]
let canStart = false
let hasNewMessages = false

const getNextWordLength = (word) => {

  let currentWidth = 0

  for (const char of word.split(``)) {

    let currentCharWidth = widthFor12ptFont[char.charCodeAt()]

    if (isNaN(currentCharWidth)) {

    currentCharWidth = 1;
    }

    currentWidth += currentCharWidth
  }

  return currentWidth
}

const shortenText = (text) => {

  let outputText = ``
  let currentWidth = 0

  for (const word of text.split(` `)) {

    let currentWordWidth = getNextWordLength(word)

    if (currentWidth + currentWordWidth + SPACE_WIDTH > MAX_WIDTH) {

    outputText = `${outputText}ENDLASTMESSAGE`
    currentWidth = 0

    // okay, but what if the word itself is greater than max width?
    if (currentWordWidth > MAX_WIDTH) {

      let splitWordWidth = 0

      for (const char of word.split(``)) {

        let currentCharWidth = widthFor12ptFont[char.charCodeAt()]

        if (isNaN(currentCharWidth)) {
      
          currentCharWidth = 1;
        }

        if (splitWordWidth + currentCharWidth > MAX_WIDTH) {

          outputText = `${outputText}ENDLASTMESSAGE`
          splitWordWidth = 0
        }

        splitWordWidth += currentCharWidth
        outputText = `${outputText}${char}`
        }

        currentWidth += splitWordWidth
        
        continue
      }
    }

    currentWidth += currentWordWidth + SPACE_WIDTH
    outputText = `${outputText} ${word}`
  }

  return outputText
}

const MAX_ROWS = 16

const splitMessages = (messages) => {

  let firstMessage = true

  if (!messages) {

    return `no messages ENDLASTMESSAGE`
  }

  for (const message of messages) {

    if (firstMessage) {

      let tempMessageOutput = `${message.chatter}: ${message.text}`

      tempMessageOutput = shortenText(tempMessageOutput)
      messageOutput = tempMessageOutput
    } else {

      let tempMessageOutput = `${message.chatter}: ${message.text}`

      tempMessageOutput = shortenText(tempMessageOutput)
      messageOutput = `${messageOutput}ENDLASTMESSAGE${tempMessageOutput}`
    }

    firstMessage = false
  }


  if (messageOutput.split(`ENDLASTMESSAGE`).length > MAX_ROWS) {

    messageOutput = messageOutput.split(`ENDLASTMESSAGE`)

    let newMessageOutput = []

    for (let i = messageOutput.length; i > messageOutput.length - MAX_ROWS; i--) {

      newMessageOutput.unshift(messageOutput[i])
    }

    messageOutput = newMessageOutput.join(`ENDLASTMESSAGE`)
  } 

  lastMessageOutput = messageOutput

  return messageOutput
}

const parseChatsToFriendlyNameString = (chats) => {

  let friendlyNameStrings = ``

  if (!chats) {

    return ``
  }

  for (let chat of chats) {

    friendlyNameStrings = `${friendlyNameStrings},${chat.friendlyName.replace(/,/g, '')}`
  }

  friendlyNameStrings = friendlyNameStrings.substring(1, friendlyNameStrings.length)

  return friendlyNameStrings
}

let lastMessageOutput

let TEST_MESSAGES = [
  {chatter: `friend 1`, text: `my super fun text message`},
  {chatter: `me`, text: `some cool old thing I said earlier`},
  {chatter: `friend 2`, text: `this message is not relevant to the conversation! not at all :(`},
  {chatter: `friend 1`, text: `i watch star wars in reverse order`},
  {chatter: `me`, text: `https://github.com/CamHenlin/MessagesForMacintosh`},
  {chatter: `friend 3`, text: `old computers are fun`}
]

const TEST_CHATS = [
  {friendlyName: `my group chat 1`, name: `my group chat 1`},
  {friendlyName: `friend 1`, name: `friend 1`},
  {friendlyName: `friend 4`, name: `friend 4`},
  {friendlyName: `boss`, name: `boss`},
  {friendlyName: `friend 3`, name: `friend 3`},
  {friendlyName: `restaurant`, name: `restaurant`},
  {friendlyName: `work`, name: `work`}
]

if (TEST_MODE) {

  // setInterval(() => {

  //   let chatWords = Math.floor(Math.random() * 20) + 1
  //   let chatMessage = ``

  //   for (let chatWord = 0; chatWord < chatWords; chatWord++) {

  //     chatMessage = `${chatMessage} ${Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, Math.floor(Math.random() * 16) + 1)}`
  //   }

  //   TEST_MESSAGES = TEST_MESSAGES.concat({chatter: `friend 1`, text: chatMessage})
  // }, 10000)
}

let storedArgsAndResults = {
  getMessages: {
    args: {},
    output: {}
  },
  hasNewMessagesInChat: {
    args: {},
    output: {}
  },
  getChats: {
    args: {},
    output: {}
  },
  getChatCounts: {
    args: {},
    output: {}
  }
}

// this is our private interface, meant to communicate with our GraphQL server and fill caches
// we want everything cached as much as possible to cut down on perceived perf issues on the 
// classic Macintosh end
class iMessageGraphClientClass {

  async getMessages (chatId, page, fromInterval) {

    storedArgsAndResults.getMessages.args = {
      chatId,
      page
    }

    if (TEST_MODE) {

      let currentLastMessageOutput = `${lastMessageOutput}`

      storedArgsAndResults.getMessages.output = splitMessages(TEST_MESSAGES)

      if (!hasNewMessages && fromInterval) {

        hasNewMessages = currentLastMessageOutput !== storedArgsAndResults.getMessages.output
      }

      return storedArgsAndResults.getMessages.output
    }

    if (DEBUG) {

      console.log(`get messages for chat ID: ${chatId}`)
    }

    let result

    try {
    
      result = await client.query({
        query: gql`query getMessages {
            getMessages(chatId: "${chatId}", page: "${page}") {
                chatter
                text
            }
        }`
      })
    } catch (error) {

      console.log(`getMessages: error with apollo query`)
      console.log(error)

      return
    }

    let messages = result.data.getMessages

    let currentLastMessageOutput = `${lastMessageOutput}`

    storedArgsAndResults.getMessages.output = splitMessages(messages)

    if (!hasNewMessages && fromInterval) {

      hasNewMessages = currentLastMessageOutput !== storedArgsAndResults.getMessages.output

      if (hasNewMessages) {

        console.log(`got new message. previous message was:`)
        console.log(currentLastMessageOutput)
        console.log(`new message set is:`)
        console.log(storedArgsAndResults.getMessages.output)
      }
    }

    return
  }

  async hasNewMessagesInChat () {

    if (!hasNewMessages) {

      return `false`
    } else {

      hasNewMessages = false
    }

    return `true`
  }

  async sendMessage (chatId, message) {

    if (TEST_MODE) {

      TEST_MESSAGES = TEST_MESSAGES.concat({chatter: `me`, text: message})

      storedArgsAndResults.getMessages.output = splitMessages(TEST_MESSAGES)

      return storedArgsAndResults.getMessages.output
    }

    let result

    try {

      message = encodeURIComponent(message)

      result = await client.query({
        query: gql`query sendMessage {
            sendMessage(chatId: "${chatId}", message: "${message}") {
                chatter
                text
            }
        }`
      })
    } catch (error) {

      console.log(`sendMessage: error with apollo query`)
      console.log(error)

      return
    }

    let messages = result.data.sendMessage

    storedArgsAndResults.getMessages.output = splitMessages(messages)

    return storedArgsAndResults.getMessages.output
  }

  async getChats () {


    if (DEBUG) {

      console.log(`getChats`)
    }

    if (TEST_MODE) {

      storedArgsAndResults.getChats.output = parseChatsToFriendlyNameString(TEST_CHATS)

      return
    }

    let result

    try {
    
      result = await client.query({
        query: gql`query getChats {
            getChats {
                name
                friendlyName
            }
        }`
      })
    } catch (error) {

      console.log(`getChats: error with apollo query`)
      console.log(error)

      return
    }

    let chats = result.data.getChats

    storedArgsAndResults.getChats.output = parseChatsToFriendlyNameString(chats)

    if (DEBUG) {

      console.log(`getChats complete`)
      console.log(storedArgsAndResults.getChats.output)
    }

    return
  }

  async getChatCounts () {

    if (DEBUG) {

      console.log(`getChatCounts`)
    }

    if (TEST_MODE) {

      storedArgsAndResults.getChatCounts.output = parseChatsToFriendlyNameString(TEST_CHATS)

      return
    }

    let result

    try {
    
      result = await client.query({
        query: gql`query getChatCounts {
            getChatCounts {
                friendlyName
                count
            }
        }`
      })
    } catch (error) {

      console.log(`getChatCounts: error with apollo query`)
      console.log(error)

      return
    }

    let chats = result.data.getChatCounts

    if (!chats) {

      return
    }

    let friendlyNameStrings = ``
  
    if (chats.length === 0) {
  
      return
    }
  
    for (let chat of chats) {
  
      friendlyNameStrings = `${friendlyNameStrings},${chat.friendlyName.replace(/,/g, '')}:::${chat.count}`
    }
  
    // remove trailing comma
    friendlyNameStrings = friendlyNameStrings.substring(1, friendlyNameStrings.length)

    storedArgsAndResults.getChatCounts.output = friendlyNameStrings

    if (DEBUG) {

      console.log(`got chat counts`)
      console.log(friendlyNameStrings)
    }

    return
  }

  setIPAddress (IPAddress) {

    console.log(`instantiate apolloclient with uri ${IPAddress}:4000/`)

    if (TEST_MODE) {

      canStart = true

      return `success`
    }

    try {

      client = new ApolloClient({
        uri: `${IPAddress}:4000/`,
        cache: new InMemoryCache(),
        link: new createHttpLink({
          uri: `${IPAddress}:4000/`
        }),
        defaultOptions
      });
    } catch (err) {
      console.log(`error instantiating the ApolloClient`)
      console.log(err)

      return `failure`
    }

    console.log(`return success`)

    canStart = true

    return `success`
  }
}

let iMessageGraphClient = new iMessageGraphClientClass()

// provide the public interface
class iMessageClient {

  constructor () {

    // kick off an update interval
    const updateInterval = setInterval(async () => {

      let intervalDate = new Date().toISOString()

      console.log(`${intervalDate}: run interval`)
    
      if (!canStart) {
    
        console.log(`${intervalDate}: can't start yet`)
    
        return
      }

      if (new Date() - lastMessageFromSerialPortTime > 30000) {

        console.log(`${intervalDate}: no serial comms for 30 seconds, unloading interval`)

        clearInterval(updateInterval)

        return
      }

      if (DEBUG) {

        console.log(`${intervalDate}: running...`)
      }

      try {
    
        if (Object.keys(storedArgsAndResults.getMessages.args).length > 0) {

          console.log(`${intervalDate}: interval: get messages for ${storedArgsAndResults.getMessages.args.chatId}`)
          await iMessageGraphClient.getMessages(storedArgsAndResults.getMessages.args.chatId, storedArgsAndResults.getMessages.args.page, true)
        }
      
        console.log(`${intervalDate}: interval: getchats`)
        await iMessageGraphClient.getChats()
        console.log(`${intervalDate}: interval: getchatcounts`)
        await iMessageGraphClient.getChatCounts()
      } catch (error) {

        console.log(`${intervalDate}: caught error when running interval`)
        console.log(error)
      }
    
      if (DEBUG) {

        console.log(`${intervalDate}: complete!`)
      }
    }, 3000)
  }

  async getMessages (chatId, page) {

    lastMessageFromSerialPortTime = new Date()

    console.log(`iMessageClient.getMessages`)

    if (storedArgsAndResults.getMessages.args.chatId !== chatId || storedArgsAndResults.getMessages.args.page !== page) {

      await iMessageGraphClient.getMessages(chatId, page, false)
    }

    console.log(`iMessageClient.getMessages, return:`)
    console.log(storedArgsAndResults.getMessages.output)

    return storedArgsAndResults.getMessages.output
  }

  async hasNewMessagesInChat (chatId) {

    lastMessageFromSerialPortTime = new Date()

    console.log(`iMessageClient.hasNewMessagesInChat`)

    let returnValue = await iMessageGraphClient.hasNewMessagesInChat(chatId)

    console.log(`iMessageClient.hasNewMessagesInChat, return:`)
    console.log(returnValue)

    return returnValue
  }

  async sendMessage (chatId, message) {

    lastMessageFromSerialPortTime = new Date()

    console.log(`iMessageClient.sendMessage(${chatId}, ${message})`)

    const messages = await iMessageGraphClient.sendMessage(chatId, message)

    return messages
  }

  async getChats () {

    lastMessageFromSerialPortTime = new Date()

    console.log(`iMessageClient.getChats`)
    
    if (Object.keys(storedArgsAndResults.getChats.output).length === 0) {

      await iMessageGraphClient.getChats()
    }

    console.log(`iMessageClient.getChats, return:`)
    console.log(storedArgsAndResults.getChats.output)

    return storedArgsAndResults.getChats.output
  }

  getChatCounts () {

    lastMessageFromSerialPortTime = new Date()

    console.log(`iMessageClient.getChatCounts, prestored return:`)
    console.log(storedArgsAndResults.getChatCounts.output)

    return storedArgsAndResults.getChatCounts.output
  }

  setIPAddress (IPAddress) {

    console.log(`iMessageClient.setIPAddress`)

    return iMessageGraphClient.setIPAddress(IPAddress)
  }
}

module.exports = iMessageClient

