/*
 * IdeaVim - Vim emulator for IDEs based on the IntelliJ platform
 * Copyright (C) 2003-2021 The IdeaVim authors
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 */
package com.maddyhome.idea.vim.ex.vimscript

import com.maddyhome.idea.vim.ex.CommandParser
import com.maddyhome.idea.vim.ex.ExException
import com.maddyhome.idea.vim.ui.VimRcFileState
import org.jetbrains.annotations.NonNls
import java.io.File
import java.io.IOException
import java.nio.file.Paths
import java.util.regex.Matcher
import java.util.regex.Pattern

/**
 * @author vlan
 */
object VimScriptParser {
  // [VERSION UPDATE] 203+ Annotation should be replaced with @NlsSafe
  @NonNls
  private const val VIMRC_FILE_NAME = "ideavimrc"

  // [VERSION UPDATE] 203+ Annotation should be replaced with @NlsSafe
  @NonNls
  private val HOME_VIMRC_PATHS = arrayOf(".$VIMRC_FILE_NAME", "_$VIMRC_FILE_NAME")

  // [VERSION UPDATE] 203+ Annotation should be replaced with @NlsSafe
  @NonNls
  private val XDG_VIMRC_PATH = "ideavim" + File.separator + VIMRC_FILE_NAME
  private val DOUBLE_QUOTED_STRING = Pattern.compile("\"([^\"]*)\"")
  private val SINGLE_QUOTED_STRING = Pattern.compile("'([^']*)'")
  private val REFERENCE_EXPR = Pattern.compile("([A-Za-z_][A-Za-z_0-9]*)")
  private val DEC_NUMBER = Pattern.compile("(\\d+)")

  // This is a pattern used in ideavimrc parsing for a long time. It removes all trailing/leading spaced and blank lines
  private val EOL_SPLIT_PATTERN = Pattern.compile(" *(\r\n|\n)+ *")

  var executingVimScript = false

  @JvmStatic
  fun findIdeaVimRc(): File? {
    val homeDirName = System.getProperty("user.home")
    // Check whether file exists in home dir
    if (homeDirName != null) {
      for (fileName in HOME_VIMRC_PATHS) {
        val file = File(homeDirName, fileName)
        if (file.exists()) {
          return file
        }
      }
    }

    // Check in XDG config directory
    val xdgConfigHomeProperty = System.getenv("XDG_CONFIG_HOME")
    val xdgConfig = if (xdgConfigHomeProperty == null || xdgConfigHomeProperty == "") {
      if (homeDirName != null) Paths.get(homeDirName, ".config", XDG_VIMRC_PATH).toFile() else null
    } else {
      File(xdgConfigHomeProperty, XDG_VIMRC_PATH)
    }
    return if (xdgConfig != null && xdgConfig.exists()) xdgConfig else null
  }

  private val newIdeaVimRcTemplate = """
    "" Source your .vimrc
    "source ~/.vimrc
    
    "" -- Suggested options --
    " Show a few lines of context around the cursor. Note that this makes the
    " text scroll if you mouse-click near the start or end of the window.
    set scrolloff=5

    " Do incremental searching.
    set incsearch

    " Don't use Ex mode, use Q for formatting.
    map Q gq
    
    
    "" -- Map IDE actions to IdeaVim -- https://jb.gg/abva4t
    "" Map \r to the Reformat Code action
    "map \r <Action>(ReformatCode)

    "" Map <leader>d to start debug
    "map <leader>d <Action>(Debug)

    "" Map \b to toggle the breakpoint on the current line
    "map \b <Action>(ToggleLineBreakpoint)
    
    
    " Find more examples here: https://jb.gg/share-ideavimrc
    
  """.trimIndent()

  fun findOrCreateIdeaVimRc(): File? {
    val found = findIdeaVimRc()
    if (found != null) return found

    val homeDirName = System.getProperty("user.home")
    if (homeDirName != null) {
      for (fileName in HOME_VIMRC_PATHS) {
        try {
          val file = File(homeDirName, fileName)
          file.createNewFile()
          file.writeText(newIdeaVimRcTemplate)
          VimRcFileState.filePath = file.absolutePath
          return file
        } catch (ignored: IOException) {
          // Try to create one of two files
        }
      }
    }
    return null
  }

  @JvmStatic
  fun executeFile(file: File): List<String> {
    val data = try {
      readFile(file)
    } catch (ignored: IOException) {
      return emptyList()
    }
    executeText(data)
    return data
  }

  fun executeText(vararg text: String) {
    executeText(listOf(*text))
  }

  fun executeText(text: List<String>) {
    for (line in text) {
      // TODO: Build a proper parse tree for a VimL file instead of ignoring potentially nested lines (VIM-669)
      if (line.startsWith(" ") || line.startsWith("\t")) continue

      val lineToExecute = if (line.startsWith(":")) line.substring(1) else line
      try {
        val command = CommandParser.parse(lineToExecute)
        val commandHandler = CommandParser.getCommandHandler(command)
        if (commandHandler is VimScriptCommandHandler) {
          commandHandler.execute(command)
        }
      } catch (ignored: ExException) {
      }
    }
  }

  @Throws(ExException::class)
  fun evaluate(expression: String, globals: Map<String?, Any?>): Any {
    // This evaluator is very basic, no proper parsing whatsoever. It is here as the very first step necessary to
    // support mapleader, VIM-650. See also VIM-669.
    var m: Matcher = DOUBLE_QUOTED_STRING.matcher(expression)
    if (m.matches()) return m.group(1)

    m = SINGLE_QUOTED_STRING.matcher(expression)
    if (m.matches()) return m.group(1)

    m = REFERENCE_EXPR.matcher(expression)
    if (m.matches()) {
      val name = m.group(1)
      val value = globals[name]
      return value ?: throw ExException("Undefined variable: $name")
    }

    m = DEC_NUMBER.matcher(expression)
    if (m.matches()) return m.group(1).toInt()

    throw ExException("Invalid expression: $expression")
  }

  @Throws(ExException::class)
  fun expressionToString(value: Any): String {
    // TODO: Return meaningful value representations
    return when (value) {
      is String -> value
      is Int -> value.toString()
      else -> throw ExException("Cannot convert '$value' to string")
    }
  }

  @Throws(IOException::class)
  fun readFile(file: File): List<String> {
    val lines = ArrayList<String>()
    file.forEachLine { line -> lineProcessor(line, lines) }
    return lines
  }

  fun readText(data: CharSequence): List<String> {
    val lines = ArrayList<String>()
    EOL_SPLIT_PATTERN.split(data).forEach { line -> lineProcessor(line, lines) }
    return lines
  }

  fun lineProcessor(line: String, lines: ArrayList<String>) {
    val trimmedLine = line.trim()
    if (trimmedLine.isBlank()) return
    lines += trimmedLine
  }
}