local p = {}
local pointmod = require('Módulo:Mapa/Pontos')
local linguistic = require('Módulo:Linguística')
local maintenance = ''
local coord  = require('Módulo:Coordenadas')
local wd = require('Módulo:Infobox/Wikidata')
local function loaddata(name)
    return require('Módulo:Mapa/dados/' .. mw.ustring.lower(name))
end

local divstyle = {
    ['clear'] = 'right',
    ['width'] ='auto',
    ['text-align'] = 'center',
    ['font-size'] = '0.9em',
    ['line-height'] = '1.4em',
    ['margin'] = '0 0 0.5em 1em',
    ['max-width'] = '325px',
    ['word-wrap'] = 'break-word',
    ['max-width'] = '99%',
    ['height'] = 'auto',
    ['justify-content'] = 'space-around',
    ['align-items'] = 'center',
}

local function addmaintenancecat(cat, sortkey) -- adicione texto à string de manutenção se houver um problema
    if mw.title.getCurrentTitle().namespace ~= 0 then
    return
    end
    maintenance = maintenance .. '[[Categoria:' .. cat .. ']]'
end

-- 'Projeção cônica com DL'
local function dllat(latitude, longitude, mapdata) -- cônica com DL
    local val =
     (mapdata.y0 +
         ( mapdata.iheight/2 - mapdata.y0 ) *
         (1 - mapdata.t *
         (latitude - mapdata.centrallat) *
         (0.01745329252 + 0.00000177219231 * (latitude - mapdata.centrallat) ^2)
         )*
         ( 1- 0.00015230871 * (longitude-mapdata.centrallong) ^2 * mapdata.s^2)
     ) / mapdata.iheight

    return val
end

local function dllong(latitude, longitude, mapdata)
    local val =
    ( (mapdata.x0 or (mapdata.iwidth/2)) +
        ( mapdata.iheight/2 - mapdata.y0 ) *
        ( 1 -mapdata.t *
        (latitude-mapdata.centrallat) *
        ( 0.01745329252 + 0.00000177219231 * (latitude-mapdata.centrallat) * (latitude-mapdata.centrallat) )
        ) *
    (longitude-mapdata.centrallong) * mapdata.s *
    (0.01745329252 - 0.000000886096156 * (longitude-(mapdata.centrallong)) * (longitude-(mapdata.centrallong))
        * mapdata.s^2
    )
    ) / mapdata.iwidth
    return val
end

--longitude equirectangular
local function equirecLong(longitude, mapdata)
    local left, right = mapdata.left, mapdata.right
    if right < left then -- se o mapa passa no meridiano 180
    right = 360 + right
    if longitude < 0 then longitude = (360 + longitude) end
    end
    return  (longitude - left) / (right - left)
end

local function equirecLat(latitude, mapdata)
    return (latitude - mapdata.top) / (mapdata.bottom - mapdata.top)   
end

----------------------------------
local function numericcoord(val) -- digitaliza coordenadas que, às vezes, estão na forma de grau/min/seg
    return tonumber(val) or tonumber(coord.dms2dec({args={val}}))
end

local function pointposition(latitude, longitude, mapdata)
    if not (latitude and longitude) then
    return nil --?
    end
  
    if longitude > 180 then -- os caprichos das coordenadas extraterrestres
    longitude = -360 + longitude
    elseif longitude < -180 then
    longitude = 360 - longitude
    end
      
    local ypos, xpos
    if mapdata.x and mapdata.y then -- para mapas complexos: calculando a posição das fórmulas no Wikicode nas chaves "x" e "y"
    xpos = mapdata.x(latitude, longitude) / 100
    ypos = mapdata.y(latitude, longitude) / 100

    elseif mapdata.projection ==  'Projeção equiretangular' or mapdata.projection ==  'Projeção equirectangular' then
    ypos = equirecLat(latitude, mapdata)
    xpos =    equirecLong(longitude, mapdata)
    elseif mapdata.projection == 'Projeção cônica com DL' then
    ypos = dllat(latitude, longitude, mapdata)
    xpos = dllong(latitude, longitude, mapdata)
    end
    return ypos, xpos
end

local function placepoint(mapdata, point) -- função ange para o ponto buildmap é uma tabela contendo latitude, longitude, tipo de ponto ...

    local ypos, xpos = pointposition(point.latitude, point.longitude, mapdata)

    if (not xpos) or (not ypos) then
    return "dados de localização inválidos"
    end

    if (ypos > 1.1) or (xpos) > 1.1 or (ypos < -0.1) or (xpos < -0.1) then
    return '[[Categoria:!Artigos com coordenadas fora do mapa]]' .. 'as coordenadas mostradas estão fora do mapa de localização solicitado'
    end
  
    local pointsize = tostring(point.pointsize or '8')
    local pointtext = point.text or ''
    local pointtype = point.pointtype or 'default'
    local pointimage = pointmod[pointtype]
    if not pointimage then
    pointimage = pointmod.default
    addmaintenancecat('!Páginas com uma predefinição de ponto de mapa não suportados')
    end

    local htmlheight = tostring(ypos * 100) .. '%'
    local htmlwidth = tostring(xpos * 100) .. '%'

    local pointdiv = mw.html.create('div')
    :css{position = 'absolute', border = 'none', top = htmlheight, left = htmlwidth}
    :tag('div')
        :css{position = 'absolute', top = '-4px', left = '-4px', ['line-height'] = '0', width = '8px'}
        :wikitext('[[Image:' .. pointimage .. '|' .. pointsize .. 'px|class=noviewer]]')
        :tag('span')
        :css{position = 'absolute', ['text-align'] = 'left', width = '150px'}
        :wikitext(pointtext)
        :done()
    :allDone()
      
    return pointdiv
end

local function buildmap(file, caption, alt, width, mapdata, pointtable, defaultpoint)
    local map = mw.html.create('div')
    :css(divstyle)
    :addClass("geobox")
    :wikitext(caption)
    :tag('table')
        :addClass('InicioMapa')
        :attr({border="0", cellspacing="0", cellpadding="0"})
        :css({margin = '0', border = 'none', padding = '0', width = 'auto'})
        :tag('tr')
            :tag('td')
            :tag('div')
                :css({position= 'relative', margin = "auto", width = '100%', ['text-align'] = 'right' })
                :wikitext('[[File:' .. file .. '|frameless|' .. width .. 'px' .. '|' .. alt .. '|class=noviewer]]' )
  
    for i, j in pairs(pointtable or{}) do -- para verficar ponto a colocar, do
    if not j.pointtype then
        j.pointtype = defaultpoint
    end
    map:node(placepoint(mapdata,j))
    end
    return map:done():done():done():done()
end

local function maxpoints(points)
    if not points then
    return nil
    end
    local minlat, maxlat, minlong, maxlong = points[1].latitude, points[1].latitude, points[1].longitude, points[1].longitude
    for i, point in ipairs(points) do
    minlat = math.min(point.latitude, minlat)
    maxlat = math.max(point.latitude, maxlat)
    minlong = math.min(point.longitude, minlong)
    maxlong = math.max(point.longitude, maxlong)
    end
    return minlat, maxlat, minlong, maxlong
    end

local function guesszoom(ids)
    if (not ids) then
    return nil
    end
    local item = ids[1]
    local area = wd.formatStatements{entity = item, property = "P2046", targetunit = "square kilometer", displayformat = "raw"}
    if (not area) or not(tonumber(area)) then
    return nil
    end
    area = tonumber(area)
    if area > 100000 then
    return 3
    end
    if area > 10000 then
    return 4
    end
    if area > 1000 then
    return 5
    end
    if area > 100 then
    return 6
    end
    return 7
end

local function guesszoom2(minlat, maxlat, minlong, maxlong)
    if not (minlat and maxlat and minlong and maxlong) or ((minlat == maxlat) and (minlong == maxlong) ) then
    return nil
    end
    local x = coord._distance({latitude = (maxlat + minlat / 2), longitude = minlong}, {latitude = (maxlat + minlat / 2), longitude =  maxlong })
    local y = coord._distance({latitude = 0, longitude = minlong}, {latitude = 0, longitude = maxlong})
    local dist = math.max(x, y) -- para ajustar se o mapa não é quadrado
    if (dist > 512) then
    return 4
    elseif (dist > 256) then
    return 5      
    elseif (dist > 128) then
    return 6
    elseif (dist> 64) then
    return 7
    elseif (dist > 32) then
    return 8
    elseif (dist > 16) then
    return 9
    elseif (dist > 8) then
    return 10
    elseif (dist > 4) then
    return 11
    elseif (dist > 2) then
    return 12
    end
    return 13
end

local function buildInteractiveMap(width, pointtable, default_zoom, ids, shapecolor)
    -- Fazemos um hack para gerar um valor padrão para latitude e longitude
    local geojson = {}


    shapecolor = shapecolor or '#800000'
    for i, point in pairs(pointtable or{}) do -- para cada ponto colocar, do
    table.insert(geojson, {
        ['type'] = 'Feature',
        ['geometry'] = {
        ['type'] = "Point",
        ['coordinates'] = { point.longitude, point.latitude }
        },
        ['properties'] = {
        ['title'] = point.text or '',
        ['marker-symbol'] = point.marker,
        ['marker-color'] =  point.markercolor or "#224422",
        }
    })

    if ids then
    local geojson2 = {
    ['type'] = 'ExternalData',
        ['service'] = 'geoshape',
      ['ids'] = ids,
      properties = {
        ['fill'] = shapecolor or '#800000',
      },
    }
    table.insert(geojson, geojson2)
    end

    end
  
    local minlat, maxlat, minlong, maxlong = maxpoints(pointtable)
    local center_lat = (maxlat + minlat) / 2
    local center_long = (maxlong + minlong) / 2
    -- 180e méridien
    if (maxlong - minlong) > 180 then
    centerlong = centerlong - 180
    end
    local args = {
        ['height'] = width,
        ['width'] = width,
        ['frameless'] = 'frameless',
        ['align'] = 'center',
        ['latitude'] = center_lat,
        ['longitude'] = center_long,
        ['zoom'] = default_zoom or guesszoom(ids) or guesszoom2(minlat, maxlat, minlong, maxlong) or 13
    }
    return mw.getCurrentFrame():extensionTag('mapframe', mw.text.jsonEncode(geojson), args)
end

local function builddynamicmap(map, maptype, width, pointtable, caption, defaultpoint, globe, default_zoom, ids, shapecolor) -- função de ajuda para multimap
    if map == '-' then
    return
    end
    if map == 'interactive' then
    if (globe and (globe ~= 'earth')) then
        return nil
    end
    return buildInteractiveMap(width, pointtable, default_zoom, ids, shapecolor)
    end

    local success, mapdata = pcall(loaddata, map)
    if not success or not mapdata.images then
    addmaintenancecat('!Páginas com dados de localização não suportados')
    end
    local name = mapdata.name or '?'

    -- análise linguística para o texto do mapa
    local datagender = mapdata.genre or ''
    local gender = string.sub(datagender, 1, 1) -- ms = masculino-singular, fp = feminino plural etc.
    local number = string.sub(datagender, 2, 2)
    local determiner = mapdata.determiner
    local ofstring = linguistic.of(name, gender, number, determiner) -- restaura "da França" ou "do Japão"

    local mapname = mapdata.name
    local file = mapdata.images[maptype] or mapdata.images['default']  or mapdata.images[1]
    if not file then
     file = mapdata.images['default']
    end
    local alt = 'ver no mapa ' .. ofstring
    local caption = 'Localização no mapa ' .. ofstring
    return buildmap(file, caption, alt, width, mapdata, pointtable, defaultpoint)
end

local function guessmaps(params)
    -- cas non terriens
    local globe = params.globe
    if globe and (globe ~= 'earth') then
    local maps = {
        moon = 'Lua',
        mars = 'Marte',
        mercury = 'Mercúrio',
        neptune = 'Neptuno',
        venus = 'Vénus',
        callisto = 'Calisto',
        ceres = 'Ceres',
        charon = 'Charon',
        enceladus = 'Encelade',
        europa = 'Europa',
        io = 'Io',
        iapetus = 'Japet',
        ganymede = 'Ganimede',
        pluto = 'Plutão',
        tethys = 'Tétis',
        titan = 'Titan',
        triton = 'Triton',
        vesta = 'Vesta',
    }
    return maps[mw.ustring.lower(globe)]
    end
  
    -- outros casos

    local data = require('Módulo:Mapa/dados')
    local validmaps = {}
    local lat = tonumber(params.latitude) or tonumber(coord.dms2dec({args={params.latitude}}))
    local long = tonumber(params.longitude) or tonumber(coord.dms2dec({args={params.longitude}}))
    if not lat or not long then return nil end
    for i, map in pairs(data) do
    if lat < map.top and lat > map.bottom then
        if map.left > map.right  then -- correção para meridiano de passagem de mapas 180
        if long < 0 then
            map.left = map.left - 360
        else
            map.right = 360 + map.right
        end
        end
        if (long > map.left and long < map.right) then
        table.insert(validmaps, map)
        end
    end
    end
    if #validmaps == 0 then
    return nil
    end
  
    local function area(map) -- função simples só para poder classificar apreciavelmente os mapas não superficiais
    return (math.abs(map.top - map.bottom)) * (math.abs(map.left- map.right))
    end
  
    table.sort(validmaps, function(a, b) return area(a) < area(b) end)

    local chosenmaps = {} -- nós não mantemos todos eles, muitas vezes seria demais

    local havezone = {} -- parâmetro "zona" do mapa já obtido, para não ter os mesmos dois tempos: { zone = {nome do mapa, posição do mapa} }

    local forbiddenzones = { -- Zona não é útil, não usar por padrão
    ['futura região francesa'] = true,
    ['frança'] = true, -- usado para áreas não administrativas, geneticamente não prático
    ['itália'] = true,
    }

    local function addmap(map, pos) -- adicione o mapa à lista e registe que ele tem um mapa com esse parâmetro "zone"
    if pos then
        chosenmaps[pos] = map.name
        havezone[map.zone] = {map, pos}
    else
        table.insert(chosenmaps, map.name)
        if map.zone then
        havezone[map.zone] = {map, #chosenmaps}
        end
    end
    end

    local function centrality(map)
    -- retorna um índice de centralidade ah hoc, mais fraco se o ponto estiver perto de uma borda
    local function compute(point, end1, end2)
        local pct = (point - end1) / (end2- end1)
        return 0.5 - math.abs(0.5 - pct)
    end
    local latcentrality = compute(lat, map.top, map.bottom)
    local longcentrality = compute(long, map.left, map.right)
    return math.min(latcentrality, longcentrality)
    end

    for i, map in pairs(validmaps) do
    if not(havezone[map.zone]) and    (not forbiddenzones[map.zone]) and #chosenmaps < 3 then
        addmap(map)
    end
    if map.zone and havezone[map.zone] and (centrality(map) > centrality(havezone[map.zone][1] ))  then -- se dois mapas tiverem o mesmo parâmetro "zona", nós tomamos o melhor centralizado
        addmap(map, havezone[map.zone][2])
    end
    end
    addmaintenancecat('!Páginas com mapas')
    return chosenmaps
end

function p.multimap(params)
    local maplist = params.maplist
    local globe = params.globe
    if not maplist and params.guessmaps ~= '-' then -- Os guessmaps podem ter outros parâmetros (escala, etc.)
    maplist = guessmaps(params)
    end
    if type(maplist) == 'string' then
    if maplist == 'não' or maplist == 'não pertinente' or maplist == 'nao' then
        return
    elseif maplist == 'interactive' and globe and globe ~= 'earth' then
        maplist = guessmaps(params)
    end
    maplist = mw.text.split(maplist, '/', true)
    end
    local staticmaps = params.staticmaps
    if type(staticmaps == 'string') then
    staticmaps = {staticmaps}
    end

    if (not maplist)  and (not staticmaps) then
    return nil
    end
    local maptype = params.maptype -- retrabalhar para quando queremos a mesma região, mas com vários tipos de mapa
    local width = params.width
    local pointtable = params.pointtable
    local caption = params.caption
    local defaultpoint = params.pointtype
    local default_zoom = params.default_zoom
    local ids = params.ids -- wikidata ids
    if not pointtable then -- tabela de pontos é a lista de pontos a serem colocados, mas quando há apenas um, podemos simplesmente ter latitude e longitude
    pointtable = {{latitude = params.latitude, longitude = params.longitude, marker = params.marker, markercolor = params.markercolor}}
    end
    for i=#pointtable, 1, -1 do
    local point = pointtable[i]
    point.latitude =  numericcoord(point.latitude)
    point.longitude =  numericcoord(point.longitude)
    if not ( point.latitude and point.longitude ) then
        table.remove( pointtable, i )
    end
    point.markercolor = point.markercolor or params.markercolor
    point.marker = point.marker or params.marker
    end
    if #pointtable == 0 then
    return
    end
    -- tratamento de largura
    if width and tonumber(width) then
    width = tonumber(width)
    else
    width = 280
    end-- se não for um número, erro ?
    local div =     mw.html.create('div'):addClass('img_toogle')
  
     --transição: veja aux [[Predefinição:Geolocalização/]] n na ausência de dados no módulo
    for i, j in ipairs(maplist or {}) do
    if j == '' then break end
    if j ~= 'interactive' then
        local success, data = pcall(loaddata, j)
        if not success then
         local mapliststring = table.concat(maplist, '/')
        return mw.getCurrentFrame():expandTemplate{title = 'Infobox/Geolocalização múltipla/transição', args = {['localizacao'] = mapliststring, type = maptype, latitude = params.latitude or pointtable[1].latitude, longitude = params.longitude or pointtable[1].longitude}}
            .. '[[Categoria:!Páginas com dados de localização não suportados]]'
        end
    end
    end
    for i = #maplist, 1, -1 do
    if maplist[i] ~= '' then
        local newmap = builddynamicmap( maplist[i], maptype, width, pointtable, caption, defaultpoint, globe, default_zoom, ids, params.shapecolor)
        div:node(newmap)
        div:tag('span'):wikitext(maintenance)
    end
    end
    for i, file in pairs(staticmaps or {}) do
    if j == '' then break end
    local caption = "Mapa de localização"
    local alt = "Ver o mapa detalhado"
    local newmap = buildmap( file, caption, alt, width)
    div:node(newmap)
    div:tag('span'):wikitext(maintenance)
    end
    return tostring(div)
end

function p.map(frame)
    local args = frame.args
    -- utilização de português
    args.maplist =  mw.text.split( args.mapa, '/', true)
    return p.multimap(args)
end

return p