import Resource from '../resource'
import Screen from '../screen'
import { Mesh } from '../mesh'
import { Texture } from '../mesh/material'
import { Node2D } from './'

import { mat4 } from 'gl-matrix'

const PAD_TOP = 0
const PAD_LEFT = 1
const PAD_BOTTOM = 2
const PAD_RIGHT = 3
const DESIRED_PADDING = 8

const SPLITTER = ' '
const NUMBER_SEPARATOR = ','

const LINE_HEIGHT = 0.03
const SPACE_ASCII = 32
const NL_ASCII = 10

class Character {
  constructor (id, xTexCoord, yTexCoord, xTexSize, yTexSize,
    xOffset, yOffset, sizeX, sizeY, xAdvance) {
    this.id = id
    this.xTexCoord = xTexCoord
    this.yTexCoord = yTexCoord
    this.xTexSize = xTexSize
    this.yTexSize = yTexSize
    this.xOffset = xOffset
    this.yOffset = yOffset
    this.sizeX = sizeX
    this.sizeY = sizeY
    this.xMaxTexCoord = xTexSize + xTexCoord
    this.yMaxTexCoord = yTexSize + yTexCoord
    this.xAdvance = xAdvance
  }
}

class Word {
  constructor (fontSize) {
    this.fontSize = fontSize
    this.characters = []
    this.width = 0
  }

  addCharacter (char) {
    if (!char) return
    this.characters.push(char)
    this.width += char.xAdvance * this.fontSize
  }
}

class Line {
  constructor (spaceWidth, fontSize, maxLength) {
    this.spaceSize = spaceWidth * fontSize
    this.maxLength = maxLength
    this.words = []
    this.lineLength = 0
  }

  attemptToAddWord (word) {
    let additionalLength = word.width
    additionalLength += this.words.length ? this.spaceSize : 0
    if (this.lineLength + additionalLength <= this.maxLength) {
      this.words.push(word)
      this.lineLength += additionalLength
      return true
    }
    return false
  }
}

class FontFile {
  constructor (name) {
    this.name = name
    this.metadata = {}
  }

  getValue (vals, key) {
    return parseInt(vals[key])
  }

  getValues (vals, key) {
    let nums = vals[key].split(NUMBER_SEPARATOR)
    let result = []
    for (let i in nums) {
      result.push(parseInt(nums[i]))
    }
    return result
  }

  // Parse a .fnt file
  readValues (data) {
    let lines = data.split('\n')
    for (let i in lines) {
      let line = lines[i]
      let lineSplit = line.split(SPLITTER)
      let lineValues = {}

      for (let j in lineSplit) {
        let valuePairs = lineSplit[j].split('=')
        if (valuePairs.length === 2) {
          lineValues[valuePairs[0]] = valuePairs[1]
        }
      }

      if (lineSplit[0] === 'info') {
        this.padding = this.getValues(lineValues, 'padding')
        this.paddingWidth = this.padding[PAD_LEFT] + this.padding[PAD_RIGHT]
        this.paddingHeight = this.padding[PAD_TOP] + this.padding[PAD_BOTTOM]
      } else if (lineSplit[0] === 'common') {
        let lineHeightPixels = this.getValue(lineValues, 'lineHeight') - this.paddingHeight
        this.vertPerPixelSize = LINE_HEIGHT / lineHeightPixels
        this.horizPixelSize = this.vertPerPixelSize / Screen.aspectRatio
        this.scaleWidth = this.getValue(lineValues, 'scaleW')
      } else if (lineSplit[0] === 'char') {
        let c = this.loadCharacter(lineValues, this.scaleWidth)
        if (c) this.metadata[c.id] = c
      }
    }
  }

  loadCharacter (values, imageSize) {
    let id = this.getValue(values, 'id')
    if (id === SPACE_ASCII) {
      this.spaceWidth = (this.getValue(values, 'xadvance') - this.paddingWidth) * this.horizPixelSize
      return null
    }
    let xTex = (this.getValue(values, 'x') + (this.padding[PAD_LEFT] - DESIRED_PADDING)) / imageSize
    let yTex = (this.getValue(values, 'y') + (this.padding[PAD_TOP] - DESIRED_PADDING)) / imageSize
    let width = this.getValue(values, 'width') - (this.paddingWidth - (2 * DESIRED_PADDING))
    let height = this.getValue(values, 'height') - ((this.paddingHeight) - (2 * DESIRED_PADDING))
    let quadWidth = width * this.horizPixelSize
    let quadHeight = height * this.vertPerPixelSize
    let xTexSize = width / imageSize
    let yTexSize = height / imageSize
    let xOff = (this.getValue(values, 'xoffset') + this.padding[PAD_LEFT] - DESIRED_PADDING) * this.horizPixelSize
    let yOff = (this.getValue(values, 'yoffset') + (this.padding[PAD_TOP] - DESIRED_PADDING)) * this.vertPerPixelSize
    let xAdvance = (this.getValue(values, 'xadvance') - this.paddingWidth) * this.horizPixelSize
    return new Character(id, xTex, yTex, xTexSize, yTexSize, xOff, yOff, quadWidth, quadHeight, xAdvance)
  }

  getCharacter (ascii) {
    return this.metadata[ascii]
  }

  static async fromFile (fontName) {
    let load = await Resource.GET('/assets/fonts/' + fontName + '.fnt')
    let file = new FontFile(fontName)
    file.readValues(load)
    return file
  }
}

class Font {
  constructor (name, metadata) {
    this.name = name
    this.metadata = metadata
    this.texture = null
  }

  // Load font data from a .fnt file and create a Font object with it
  static async fromFile (name) {
    let meta = await FontFile.fromFile(name)
    return new Font(name, meta)
  }

  // Load font texture
  async loadTextures (gl) {
    this.texture = await Texture.createTexture2D(gl, await Resource.loadImage('/assets/fonts/' + this.name + '.png'), false, gl.LINEAR)
  }

  // Create a renderable mesh for a text
  createTextMesh (gl, text) {
    let lines = this.createStructure(text)
    let data = this.createQuadVertices(text, lines)
    return Mesh.constructFromVerticesUVs(gl, data.vertices, data.textureCoords)
  }

  // Create a structure of lines, words and characters in order to generate the vertices properly
  createStructure (text) {
    let chars = text.asCharacters
    let lines = []
    let currentLine = new Line(this.metadata.spaceWidth, text.fontSize, text.lineLength)
    let currentWord = new Word(text.fontSize)
    for (let c in chars) {
      let ascii = parseInt(chars[c])
      if (ascii === SPACE_ASCII) {
        let added = currentLine.attemptToAddWord(currentWord)
        if (!added) {
          lines.push(currentLine)
          currentLine = new Line(this.metadata.spaceWidth, text.fontSize, text.lineLength)
          currentLine.attemptToAddWord(currentWord)
        }
        currentWord = new Word(text.fontSize)
        continue
      }

      if (ascii === NL_ASCII) {
        let added = currentLine.attemptToAddWord(currentWord)
        if (!added) {
          lines.push(currentLine)
          currentLine = new Line(this.metadata.spaceWidth, text.fontSize, text.lineLength)
          currentLine.attemptToAddWord(currentWord)
        }
        lines.push(currentLine)
        currentLine = new Line(this.metadata.spaceWidth, text.fontSize, text.lineLength)
        currentWord = new Word(text.fontSize)
        continue
      }

      let character = this.metadata.getCharacter(ascii)
      currentWord.addCharacter(character)
    }
    this.completeStructure(lines, currentLine, currentWord, text)
    return lines
  }

  // Add final word
  completeStructure (lines, currentLine, currentWord, text) {
    let added = currentLine.attemptToAddWord(currentWord)
    if (!added) {
      lines.push(currentLine)
      currentLine = new Line(this.metadata.spaceWidth, text.fontSize, text.lineLength)
      currentLine.attemptToAddWord(currentWord)
    }
    lines.push(currentLine)
    return lines
  }

  // Create text vertices
  createQuadVertices (text, lines) {
    text.lines = lines.length
    let cursorX = 0
    let cursorY = 0
    let vertices = []
    let textureCoords = []
    for (let i in lines) {
      let line = lines[i]
      if (text.centered) {
        cursorX = (line.maxLength - line.lineLength) / 2
      }
      for (let j in line.words) {
        let word = line.words[j]
        for (let k in word.characters) {
          let letter = word.characters[k]
          this.addVerticesForCharacter(cursorX, cursorY, letter, text.fontSize, vertices)
          this.addTexCoords(textureCoords, letter.xTexCoord, letter.yTexCoord, letter.xMaxTexCoord, letter.yMaxTexCoord)
          cursorX += letter.xAdvance * text.fontSize
        }
        cursorX += this.metadata.spaceWidth * text.fontSize
      }
      cursorX = 0
      cursorY += LINE_HEIGHT * text.fontSize
    }
    return { vertices, textureCoords }
  }

  // Create text vertex coordinates for a specific character
  addVerticesForCharacter (cursorX, cursorY, character, fontSize, vertices) {
    let x = cursorX + (character.xOffset * fontSize)
    let y = cursorY + (character.yOffset * fontSize)
    let maxX = x + (character.sizeX * fontSize)
    let maxY = y + (character.sizeY * fontSize)
    let properX = (2 * x) - 1
    let properY = (-2 * y) + 1
    let properMaxX = (2 * maxX) - 1
    let properMaxY = (-2 * maxY) + 1
    this.addVertices(vertices, properX, properY, properMaxX, properMaxY)
  }

  // Create text vertex coordinates
  addVertices (vertices, x, y, maxX, maxY) {
    vertices.push(x)
    vertices.push(y)
    vertices.push(x)
    vertices.push(maxY)
    vertices.push(maxX)
    vertices.push(maxY)
    vertices.push(maxX)
    vertices.push(maxY)
    vertices.push(maxX)
    vertices.push(y)
    vertices.push(x)
    vertices.push(y)
  }

  // Create text texture coordinates
  addTexCoords (texCoords, x, y, maxX, maxY) {
    texCoords.push(x)
    texCoords.push(y)
    texCoords.push(x)
    texCoords.push(maxY)
    texCoords.push(maxX)
    texCoords.push(maxY)
    texCoords.push(maxX)
    texCoords.push(maxY)
    texCoords.push(maxX)
    texCoords.push(y)
    texCoords.push(x)
    texCoords.push(y)
  }
}

class GUIText extends Node2D {
  constructor (text, font, fontSize, pos, size, centered = false) {
    super(pos, size)
    this.text = text
    this.fontSize = fontSize
    this.font = font
    this.centered = centered

    this.color = [0.0, 0.0, 0.0]

    this.width = 0.45
    this.edge = 0.1

    this.outline = {
      width: 0.0,
      edge: 0.5,
      color: [0.0, 0.0, 0.0],
      offset: [0.0, 0.0]
    }
  }

  // Do not scale the transform like we do with regular GUIs
  updateTransform () {
    let matrix = mat4.create()
    mat4.translate(matrix, matrix, [this.pos[0], this.pos[2], 0.0])
    if (this.rotation !== 0.0) {
      mat4.rotate(matrix, matrix, this.rotation * Math.PI / 180, [0.0, 0.0, 1.0])
    }

    // Add parent's transform to this
    if (this.parent) {
      mat4.mul(matrix, this.parent.transform, matrix)
    }

    // Set the matrix
    this.transform = matrix

    // Update children's transforms
    for (let i in this.children) {
      let child = this.children[i]
      if (!(child instanceof Node2D)) continue
      child.updateTransform()
    }
  }

  setText (str) {
    this.text = str
    this.mesh = null
  }

  createMesh (gl) {
    this.mesh = this.font.createTextMesh(gl, this)
  }

  get lineLength () {
    return this.size[2]
  }

  // Return all characters as their ascii code
  get asCharacters () {
    let chars = []
    for (let i = 0; i < this.text.length; i++) {
      chars.push(this.text.charCodeAt(i))
    }
    return chars
  }

  draw (gl, shader) {
    // Let only the font shader be used to render text
    if (shader.name !== 'font') return super.draw(gl, shader)
    if (!this.text) return
    const transformLocation = shader.getUniformLocation(gl, 'uTransformation')
    const cLocation = shader.getUniformLocation(gl, 'uColor')
    const wLocation = shader.getUniformLocation(gl, 'uWidth')
    const eLocation = shader.getUniformLocation(gl, 'uEdge')
    const owLocation = shader.getUniformLocation(gl, 'uOutlineWidth')
    const oeLocation = shader.getUniformLocation(gl, 'uOutlineEdge')
    const ooLocation = shader.getUniformLocation(gl, 'uOutlineOffset')
    const ocLocation = shader.getUniformLocation(gl, 'uOutlineColor')
    gl.uniformMatrix4fv(transformLocation, false, this.transform)

    gl.uniform1f(wLocation, this.width)
    gl.uniform1f(eLocation, this.edge)

    gl.uniform1f(owLocation, this.outline.width)
    gl.uniform1f(oeLocation, this.outline.edge)
    gl.uniform2fv(ooLocation, this.outline.offset)

    gl.uniform3fv(cLocation, this.color)
    gl.uniform3fv(ocLocation, this.outline.color)

    if (!this.mesh) this.createMesh(gl)
    this.mesh.prepare(gl, shader)
    this.mesh.draw(gl, shader)
    this.mesh.postdraw(gl, shader)
  }
}

class FontRenderer {
  discoverTextNodes (nodes) {
    let textNodes = []
    for (let i in nodes) {
      let node = nodes[i]
      if (!(node instanceof GUIText)) {
        if (node.children.length) {
          textNodes = textNodes.concat(this.discoverTextNodes(node.children))
        }
        continue
      }
      textNodes.push(node)
    }
    return textNodes
  }

  draw (gl, cam, nodes) {
    // Discover all nodes in the array that are texts
    let fontPairs = {}
    let textNodes = this.discoverTextNodes(nodes)
    for (let i in textNodes) {
      let node = textNodes[i]
      if (!this.fonts) {
        this.fonts = {}
      }

      if (!this.fonts[node.font.name]) {
        this.fonts[node.font.name] = node.font
      }

      if (fontPairs[node.font.name]) {
        fontPairs[node.font.name].push(node)
      } else {
        fontPairs[node.font.name] = [node]
      }
    }

    // Start rendering individual text arrays
    this.shader.use(gl)
    gl.enable(gl.BLEND)
    gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
    gl.disable(gl.DEPTH_TEST)
    for (let i in fontPairs) {
      let texts = fontPairs[i]
      let font = this.fonts[i]
      // Set font's map as the texture
      gl.activeTexture(gl.TEXTURE0)
      gl.bindTexture(font.texture.type, font.texture.id)
      // Draw all texts
      for (let j in texts) {
        let text = texts[j]
        text.draw(gl, this.shader)
      }
    }
    gl.enable(gl.DEPTH_TEST)
    gl.disable(gl.BLEND)
  }

  async initialize (game) {
    this.shader = await game.shaders.createShaderFromFiles(game.gl, 'font', false)
  }
}

export { FontRenderer, GUIText, Font }
