-- mod-version:3
--[[
  language_php.lua
  provides php syntax support allowing mixed html, css and js
  version: 20220614_1
--]]
local syntax = require "core.syntax"
local common = require "core.common"
local config = require "core.config"

-- load syntax dependencies to add additional rules
require "plugins.language_css"
require "plugins.language_js"

local psql_found = pcall(require, "plugins.language_psql")
local sql_strings = {}

config.plugins.language_php = common.merge({
  sql_strings = true,
  -- The config specification used by the settings gui
  config_spec = {
    name = "Language PHP",
    {
      label = "SQL Strings",
      description = "Highlight as SQL, strings starting with sql statements, "
        .. "depends on language_psql.",
      path = "sql_strings",
      type = "toggle",
      default = true,
      on_apply = function(enabled)
        local syntax_php = syntax.get("file.phps")
        if enabled and psql_found then
          if
            not syntax_php.patterns[6].syntax
            or
            syntax_php.patterns[6].syntax ~= '.sql'
          then
            table.insert(syntax_php.patterns, 5, sql_strings[1])
            table.insert(syntax_php.patterns, 6, sql_strings[2])
          end
        elseif
          syntax_php.patterns[6].syntax
          and
          syntax_php.patterns[6].syntax == '.sql'
        then
          table.remove(syntax_php.patterns, 5)
          table.remove(syntax_php.patterns, 5)
        end
      end
    }
  }
}, config.plugins.language_php)

-- Patterns to match some of the string inline variables
local inline_variables = {
  { pattern = "%s+",           type = "string" },
  { pattern = "\\%$",          type = "string" },
  { pattern = "%{[%$%s]*%}",   type = "string" },
  -- matches {$varname[index]}
  { pattern = "{"
      .. "()%$[%a_][%w_]*"
      .. "()%["
      .. "()[%w%s_%-\"\'%(%)|;:,%.#@%%!%^&%*%+=%[%]<>~`%?\\/]*"
      .. "()%]"
      .. "}",
    type = {
      "keyword", "keyword2", "keyword", "string", "keyword"
    }
  },
  { pattern = "{"
      .. "()%$[%a_][%w_]*"
      .. "()%->"
      .. "()[%a_][%w_]*"
      .. "()}",
    type = {
      "keyword", "keyword2", "keyword", "symbol", "keyword"
    }
  },
  { pattern = "{()%$[%a_][%w_]*()}",
    type = { "keyword", "keyword2", "keyword" }
  },
  { pattern = "%$[%a_][%w_]*()%[()[%w_]*()%]",
    type = { "keyword2", "keyword", "string", "keyword" }
  },
  { pattern = "%$[%a_][%w_]*()%->()%a[%w_]*",
    type = { "keyword2", "keyword", "symbol" }
  },
  { pattern = "%$[%a_][%w_]*", type = "keyword2" },
  { pattern = "%w+",           type = "string" }
}

local function combine_patterns(t1, t2)
  local temp = { table.unpack(t1) }
  for _, t in ipairs(t2) do
    table.insert(temp, t)
  end
  return temp
end

local function clone(tbl)
  local t = {}
  if tbl then
    for k, v in pairs(tbl) do
      if type(v) == "table" then
        t[k] = clone(v)
      else
        t[k] = v
      end
    end
  end
  return t
end

-- optionally allow sql syntax on strings
if psql_found then
  -- generate SQL string marker regex
  local sql_markers = { 'create', 'select', 'insert', 'update', 'replace', 'delete', 'drop', 'alter' }
  local sql_regex = table.concat(sql_markers, '|')

  -- inject inline variable rules to cloned psql syntax
  local syntax_phpsql = clone(syntax.get("file.sql"))
  syntax_phpsql.name = "PHP SQL"
  syntax_phpsql.files = "%.phpsql$"
  table.insert(syntax_phpsql.patterns, 2, { pattern = "\\%$", type = "symbol" })
  table.insert(syntax_phpsql.patterns, 3, { pattern = "%{[%$%s]*%}", type = "symbol" })
  for i=4, 9 do
    table.insert(syntax_phpsql.patterns, i, inline_variables[i])
  end

  -- SQL strings
  sql_strings = {
    {
        regex  = { '"(?=[\\s(]*(?i:'..sql_regex..')\\s+)', '"', '\\' },
        syntax = syntax_phpsql,
        type   = "string"
    },
    {
        regex  = { "'(?=[\\s(]*(?i:"..sql_regex..")\\s+)", '\'', '\\' },
        syntax = '.sql',
        type   = "string"
    },
  }
end

-- define the core php syntax coloring
syntax.add {
  name = "PHP Source",
  files = { "%.phps$" },
  headers = "^<%?php",
  comment = "//",
  block_comment = {"/*", "*/"},
  patterns = {
    -- Attributes
    { pattern = {"#%[", "%]"},               type = "normal"   },
    -- Comments
    { pattern = "//.-\n",                    type = "comment"  },
    { pattern = "#.-\n",                     type = "comment"  },
    { pattern = { '/%*%*%s+', '%*/'}, -- Subsyntax for Doc comments
      syntax = {
        patterns = {
          { pattern = "@author%s+()[%w%d%s%._]+()%s+%b<>",
            type = { "annotation", "annotation.literal", "annotation.string" }
          },
          { pattern = "@author%s+()[%w%d%s%._]+",
            type = { "annotation", "annotation.literal" }
          },
          { pattern = "@category%s+()%S+",
            type = { "annotation", "annotation.string" }
          },
          { pattern = "@deprecated%s+()%d+[%d%.]*()%s+.*",
            type = { "annotation", "annotation.number", "comment" }
          },
          { pattern = "@example%s+()%S+()%s+%d+()%s+%d+",
            type = {
              "annotation", "annotation.string",
              "annotation.number", "annotation.number"
            }
          },
          { pattern = "@example%s+()%S+()%s+%d+",
            type = { "annotation", "annotation.string", "annotation.number" }
          },
          { pattern = "@example%s+()%S+",
            type = { "annotation", "annotation.string" }
          },
          { pattern = "@global%s+()%S+()%s+%S+",
            type = { "annotation", "annotation.literal", "annotation.keyword2" }
          },
          { pattern = "@global%s+()%S+",
            type = { "annotation", "annotation.literal" }
          },
          { pattern = "@license%s+()%S+()%s.*",
            type = { "annotation", "annotation.string", "annotation.literal" }
          },
          { pattern = "@link%s+()%S+()%s.*",
            type = { "annotation", "annotation.string", "annotation.literal" }
          },
          { pattern = "@package%s+()%S+",
            type = { "annotation", "annotation.literal" }
          },
          { pattern = "@param%s+()%S+()%s+%$%S+",
            type = { "annotation", "annotation.literal", "annotation.keyword2" }
          },
          { pattern = "@param%s+()%$%S+",
            type = { "annotation", "annotation.keyword2" }
          },
          { regex = "@property(?:\\-read|\\-write)?\\s+()[^\\s]+()\\s+\\$[^\\s]+",
            type = { "annotation", "annotation.literal", "annotation.keyword2" }
          },
          { regex = "@property(?:\\-read|\\-write)?\\s+()\\$[^\\s]+",
            type = { "annotation", "annotation.keyword2" }
          },
          { regex = "@property(?:\\-read|\\-write)?\\s+()[^\\s]+",
            type = { "annotation", "annotation.literal" }
          },
          { pattern = "@return%s+()%S+",
            type = { "annotation", "annotation.literal" }
          },
          { pattern = "@see%s+()%S+()%s.*",
            type = { "annotation", "annotation.literal", "comment" }
          },
          { pattern = "@since%s+()%d+[%d%.]*()%s+.*",
            type = { "annotation", "annotation.number", "comment" }
          },
          { pattern = "@source%s+()%d+()%s+%d+",
            type = { "annotation", "annotation.number", "annotation.number" }
          },
          { pattern = "@source%s+()%d+",
            type = { "annotation", "annotation.number" }
          },
          { pattern = "@subpackage%s+()%S+",
            type = { "annotation", "annotation.literal" }
          },
          { pattern = "@throws%s+()%S+",
            type = { "annotation", "annotation.literal" }
          },
          { pattern = "@uses?%s+()%S+()%s.*",
            type = { "annotation", "annotation.literal", "comment" }
          },
          { pattern = "@used%-by%s+()%S+()%s.*",
            type = { "annotation", "annotation.literal", "comment" }
          },
          { pattern = "@var%s+()%S+()%s+%$%S+",
            type = { "annotation", "annotation.literal", "annotation.keyword2" }
          },
          { pattern = "@var%s+()%$%S+",
            type = { "annotation", "annotation.keyword2" }
          },
          { pattern = "@var%s+()%S+",
            type = { "annotation", "annotation.literal" }
          },
          { pattern = "@version%s+()%d+[%d%.]*()%s.*",
            type = { "annotation", "annotation.number", "comment" }
          },
          { pattern = "@version%s+()%S+: %b$$",
            type = { "annotation", "annotation.number" }
          },
          { pattern = "@version%s+()%b@@",
            type = { "annotation", "annotation.number" }
          },
          { pattern = "@version%s+()%b$$",
            type = { "annotation", "annotation.number" }
          },
          { pattern = "@[%a]+[%w%-]*",  type = "comment" },
          { pattern = ".",              type = "comment" }
        },
        symbols = {
          ["@api"] = "annotation",
          ["@author"] = "annotation",
          ["@category"] = "annotation",
          ["@copyright"] = "annotation",
          ["@deprecated"] = "annotation",
          ["@example"] = "annotation",
          ["@filesource"] = "annotation",
          ["@final"] = "annotation",
          ["@global"] = "annotation",
          ["@ignore"] = "annotation",
          ["@internal"] = "annotation",
          ["@license"] = "annotation",
          ["@link"] = "annotation",
          ["@method"] = "annotation",
          ["@property"] = "annotation",
          ["@property-read"] = "annotation",
          ["@property-write"] = "annotation",
          ["@see"] = "annotation",
          ["@since"] = "annotation",
          ["@source"] = "annotation",
          ["@subpackage"] = "annotation",
          ["@package"] = "annotation",
          ["@param"] = "annotation",
          ["@return"] = "annotation",
          ["@throws"] = "annotation",
          ["@todo"] = "annotation",
          ["@use"] = "annotation",
          ["@uses"] = "annotation",
          ["@used-by"] = "annotation",
          ["@version"] = "annotation",
          ["@var"] = "annotation"
        }
      },
      type = "comment"
    },
    { pattern = { "/%*", "%*/" },            type = "comment"  },
    -- Single quote string
    { pattern = { "'", "'", '\\' },          type = "string"   },
    { pattern = { "<<<'%a%w*'\n", "^%s*%a%w*%f[;]", '\\' },
      type = "string"
    },
    -- Strings with support for some inline variables syntax
    { pattern = { "<<<%a%w*\n", "^%s*%a%w*%f[;]", '\\' },
      syntax = {
        patterns = combine_patterns(inline_variables, {
          -- prevent matching outside of the parent string
          { pattern = "^%s*%a%w*();$",
            type = { "string", "normal" }
          },
          { pattern = "%p", type = "string" },
        }),
        symbols = {}
      },
      type = "string"
    },
    { pattern = { '"', '"', '\\' },
      syntax = {
        patterns = combine_patterns(inline_variables, {
          -- prevent matching outside of the parent string
          { pattern = "[^\"]",         type = "string" },
          { pattern = "%p+%f[\"]",     type = "string" },
          { pattern = "%p",            type = "string" },
        }),
        symbols = {}
      },
      type = "string"
    },
    { pattern = { '`', '`', '\\' },
      syntax = {
        patterns = combine_patterns(inline_variables, {
          -- prevent matching outside of the parent string
          { pattern = "[^`]",          type = "string" },
          { pattern = "%p+%f[`]",      type = "string" },
          { pattern = "%p",            type = "string" },
        }),
        symbols = {}
      },
      type = "string"
    },
    { pattern = "0[bB][%d]+",                 type = "number"   },
    { pattern = "0[xX][%da-fA-F]+",           type = "number"   },
    { pattern = "-?%d[%d_%.eE]*",             type = "number"   },
    { pattern = "-?%.?%d+",                   type = "number"   },
    { pattern = "[%.%+%-=/%*%^%%<>!~|&%?:@]", type = "operator" },
     -- Variables
    { pattern = "%$[%w_]+",                   type = "keyword2" },
    -- Respect control structures, treat as keyword not function
    { pattern = "if[%s]*%f[(]",               type = "keyword"  },
    { pattern = "else[%s]*%f[(]",             type = "keyword"  },
    { pattern = "elseif[%s]*%f[(]",           type = "keyword"  },
    { pattern = "for[%s]*%f[(]",              type = "keyword"  },
    { pattern = "foreach[%s]*%f[(]",          type = "keyword"  },
    { pattern = "while[%s]*%f[(]",            type = "keyword"  },
    { pattern = "catch[%s]*%f[(]",            type = "keyword"  },
    { pattern = "switch[%s]*%f[(]",           type = "keyword"  },
    { pattern = "match[%s]*%f[(]",            type = "keyword"  },
    { pattern = "fn[%s]*%f[(]",               type = "keyword"  },
    -- All functions that aren't control structures
    { pattern = "[%a_][%w_]*[%s]*%f[(]",      type = "function" },
    -- Array type hint not added on symbols to also make it work
    -- as a function call
    { pattern = "array",                      type = "literal"  },
    -- Match static or namespace container on sub element access
    { pattern = "[%a_][%w_]*[%s]*%f[:]",      type = "literal"  },
    -- Uppercase constants of at least 2 chars in len
    {
        pattern = "%u[%u_][%u%d_]*%f[%s%+%*%-%.%(%)%?%^%%=/<>~|&;:,!]",
        type = "number"
    },
    -- Magic constants
    { pattern = "__[%u]+__",                  type = "number"   },
    -- Everything else
    { pattern = "[%a_][%w_]*",                type = "symbol"   },
  },
  symbols = {
    ["return"] = "keyword",
    ["if"] = "keyword",
    ["else"] = "keyword",
    ["elseif"] = "keyword",
    ["endif"] = "keyword",
    ["declare"] = "keyword",
    ["enddeclare"] = "keyword",
    ["switch"] = "keyword",
    ["endswitch"] = "keyword",
    ["as"] = "keyword",
    ["do"] = "keyword",
    ["for"] = "keyword",
    ["endfor"] = "keyword",
    ["foreach"] = "keyword",
    ["endforeach"] = "keyword",
    ["while"] = "keyword",
    ["endwhile"] = "keyword",
    ["match"] = "keyword",
    ["case"] = "keyword",
    ["continue"] = "keyword",
    ["default"] = "keyword",
    ["break"] = "keyword",
    ["goto"] = "keyword",
    ["yield"] = "keyword",

    ["try"] = "keyword",
    ["catch"] = "keyword",
    ["throw"] = "keyword",
    ["finally"] = "keyword",

    ["class"] = "keyword",
    ["enum"] = "keyword",
    ["trait"] = "keyword",
    ["interface"] = "keyword",
    ["public"] = "keyword",
    ["static"] = "keyword",
    ["protected"] = "keyword",
    ["private"] = "keyword",
    ["readonly"] = "keyword",
    ["abstract"] = "keyword",
    ["final"] = "keyword",
    ["$this"] = "literal",

    ["function"] = "keyword",
    ["fn"] = "keyword",
    ["global"] = "keyword",
    ["var"] = "keyword",
    ["const"] = "keyword",

    ["bool"] = "literal",
    ["boolean"] = "literal",
    ["int"] = "literal",
    ["integer"] = "literal",
    ["real"] = "literal",
    ["double"] = "literal",
    ["float"] = "literal",
    ["string"] = "literal",
    ["object"] = "literal",
    ["callable"] = "literal",
    ["iterable"] = "literal",
    ["void"] = "literal",
    ["parent"] = "literal",
    ["self"] = "literal",
    ["mixed"] = "literal",
    ["never"] = "literal",

    ["namespace"] = "keyword",
    ["extends"] = "keyword",
    ["implements"] = "keyword",
    ["instanceof"] = "keyword",
    ["require"] = "keyword",
    ["require_once"] = "keyword",
    ["include"] = "keyword",
    ["include_once"] = "keyword",
    ["use"] = "keyword",
    ["new"] = "keyword",
    ["clone"] = "keyword",

    ["true"] = "number",
    ["false"] = "number",
    ["NULL"] = "number",
    ["null"] = "number",

    ["print"] = "function",
    ["echo"] = "function",
    ["exit"] = "function",
  },
}

-- insert sql string rules after the "/%*", "%*/" pattern
if psql_found and config.plugins.language_php.sql_strings then
  local syntax_php = syntax.get("file.phps")
  table.insert(syntax_php.patterns, 5, sql_strings[1])
  table.insert(syntax_php.patterns, 6, sql_strings[2])
end

-- allows html, css and js coloring on php files
syntax.add {
  name = "PHP",
  files = { "%.php$", "%.phtml" },
  block_comment = { "<!--", "-->" },
  patterns = {
    {
      regex = {
        "<\\?php\\s+",
        "(?:\\?>|(?=`{3}))" -- end if inside markdown code tags
      },
      syntax = ".phps",
      type = "keyword2"
    },
    {
      pattern = {
        "<%?=?",
        "%?>"
      },
      syntax = ".phps",
      type = "keyword2"
    },
    {
      pattern = {
        "<%s*[sS][cC][rR][iI][pP][tT]%f[%s>].->",
        "<%s*/%s*[sS][cC][rR][iI][pP][tT]%s*>"
      },
      syntax = ".js",
      type = "function"
    },
    {
      pattern = {
        "<%s*[sS][tT][yY][lL][eE]%f[%s>].->",
        "<%s*/%s*[sS][tT][yY][lL][eE]%s*>"
      },
      syntax = ".css",
      type = "function"
    },
    { pattern = { "<!%-%-", "%-%->" },     type = "comment"  },
    { pattern = { '%f[^>][^<]', '%f[<]' }, type = "normal"   },
    { pattern = { '"', '"', '\\' },        type = "string"   },
    { pattern = { "'", "'", '\\' },        type = "string"   },
    { pattern = "0x[%da-fA-F]+",           type = "number"   },
    { pattern = "-?%d+[%d%.]*f?",          type = "number"   },
    { pattern = "-?%.?%d+f?",              type = "number"   },
    { pattern = "%f[^<]![%a_][%w_]*",      type = "keyword2" },
    { pattern = "%f[^<][%a_][%w_]*",       type = "function" },
    { pattern = "%f[^<]/[%a_][%w_]*",      type = "function" },
    { pattern = "[%a_][%w_]*",             type = "keyword"  },
    { pattern = "[/<>=]",                  type = "operator" },
    -- match markdown code tags to be able to end php highlighting
    -- when inside the subsyntax .phps
    { regex = "(?=`{3})",                  type = "string"   }
  },
  symbols = {},
}

-- allow coloring of php code inside css and js code
local syntaxes = { "css", "js" }
for _, ext in pairs(syntaxes) do
  local syntax_table = syntax.get("file."..ext, "")

  table.insert(
    syntax_table.patterns,
    1,
    {
      pattern = {
        "<%?=?",
        "%?>"
      },
      syntax = ".phps",
      type = "keyword2"
    }
  )

  table.insert(
    syntax_table.patterns,
    1,
    {
      pattern = {
        "<%?php%s+",
        "%?>"
      },
      syntax = ".phps",
      type = "keyword2"
    }
  )
end
