--         Hologram Editor
-- by NEO, Totoro
-- 10/14/2014, all right reserved =)
-- райтс, хуяйтс, резервед ЙОПТА

local MineOSCore = require("MineOSCore")
local unicode = require('unicode')
local event = require('event')
local term = require('term')
local fs = require('filesystem')
local com = require('component')
local gpu = com.gpu

local lang = MineOSCore.getCurrentApplicationLocalization()

--   Константы   --
HOLOH = 32
HOLOW = 48

--     Цвета     --
backcolor = 0x000000
forecolor = 0xFFFFFF
infocolor = 0x0066FF
errorcolor = 0xFF0000
helpcolor = 0x006600
graycolor = 0x080808
goldcolor = 0xFFDF00
--      ***      --


-- загружаем доп. оборудование
function trytofind(name)
  if com.isAvailable(name) then
    return com.getPrimary(name)
  else
    return nil
  end
end

local h = trytofind('hologram')

-- ========================================= H O L O G R A P H I C S ========================================= --
holo = {}
function set(x, y, z, value)
  if holo[x] == nil then holo[x] = {} end
  if holo[x][y] == nil then holo[x][y] = {} end
  holo[x][y][z] = value
end
function get(x, y, z)
  if holo[x] ~= nil and holo[x][y] ~= nil and holo[x][y][z] ~= nil then 
    return holo[x][y][z]
  else
    return 0
  end
end

function save(filename)
  -- сохраняем палитру
  file = io.open(filename, 'wb')
  for i=1, 3 do
    for c=1, 3 do
      file:write(string.char(colortable[i][c]))
    end
  end
  -- сохраняем массив
  for x=1, HOLOW do
    for y=1, HOLOH do
      for z=1, HOLOW, 4 do
        a = get(x,y,z)
        b = get(x,y,z+1)
        c = get(x,y,z+2)
        d = get(x,y,z+3)
        byte = d*64 + c*16 + b*4 + a
        file:write(string.char(byte))
      end
    end
  end
  file:close()
end

local function load(filename)
  if fs.exists(filename) then
    file = io.open(filename, 'rb')
    -- загружаем палитру
    for i=1, 3 do
      for c=1, 3 do
        colortable[i][c] = string.byte(file:read(1))
      end
      setHexColor(i,colortable[i][1],
                    colortable[i][2],
                    colortable[i][3])
    end
    -- загружаем массив
    holo = {}
    for x=1, HOLOW do
      for y=1, HOLOH do
        for z=1, HOLOW, 4 do
          byte = string.byte(file:read(1))
          for i=0, 3 do
            a = byte % 4
            byte = math.floor(byte / 4)
            if a ~= 0 then set(x,y,z+i, a) end
          end
        end
      end
    end
    file:close()
    return true
  else
    --print("[ОШИБКА] Файл "..filename.." не найден.")
    return false
  end
end


-- ============================================= G R A P H I C S ============================================= --
-- проверка разрешения экрана, для комфортной работы необходимо разрешение > HOLOW по высоте и ширине
OLDWIDTH, OLDHEIGHT = gpu.getResolution()
WIDTH, HEIGHT = gpu.maxResolution()
if HEIGHT < HOLOW+2 then
  error(lang.badGPU)
else
  WIDTH = HOLOW*2+40
  HEIGHT = HOLOW+2
  gpu.setResolution(WIDTH, HEIGHT)
end
gpu.setForeground(forecolor)
gpu.setBackground(backcolor)

-- рисуем линию
local strLine = "+"
for i=1, WIDTH do
  strLine = strLine..'-'
end
function line(x1, x2, y)
  gpu.set(x1,y,string.sub(strLine, 1, x2-x1))
  gpu.set(x2,y,'+')
end

-- рисуем фрейм
function frame(x1, y1, x2, y2, caption)
  line(x1, x2, y1)
  line(x1, x2, y2)

  if caption ~= nil then
    gpu.set(x1+(x2-x1)/2-unicode.len(caption)/2, y1, caption)
  end
end

-- рисуем сетку
local strGrid = ""
for i=1, HOLOW/2 do
  strGrid = strGrid.."██  "
end
function drawGrid(x, y)
  gpu.fill(0, y, MENUX, HOLOW, ' ')
  gpu.setForeground(graycolor)
  for i=0, HOLOW-1 do
    if view>0 and i==HOLOH then 
      gpu.setForeground(forecolor)
      line(1, MENUX-1, y+HOLOH)
      break
    end
    gpu.set(x+(i%2)*2, y+i, strGrid)
  end
  if view == 0 then gpu.setForeground(forecolor) end
end

-- рисуем цветной прямоугольник
function drawRect(x, y, color)
  gpu.set(x, y,   "╓──────╖")
  gpu.set(x, y+1, "║      ║")
  gpu.set(x, y+2, "╙──────╜")
  gpu.setForeground(color)
  gpu.set(x+2, y+1, "████")
  gpu.setForeground(forecolor)
end

MENUX = HOLOW*2+5
BUTTONW = 12

-- рисуем меню выбора "кисти"
function drawColorSelector()
  frame(MENUX, 3, WIDTH-2, 16, lang.palette)
  for i=0, 3 do
    drawRect(MENUX+1+i*8, 5, hexcolortable[i])
  end
  gpu.set(MENUX+1, 10, "R:")
  gpu.set(MENUX+1, 11, "G:")
  gpu.set(MENUX+1, 12, "B:")
end
function drawColorCursor(force)
  if brush.color*8 ~= brush.x then brush.x = brush.color*8 end
  if force or brush.gx ~= brush.x then
    gpu.set(MENUX+1+brush.gx, 8, "        ")
    if brush.gx < brush.x then brush.gx = brush.gx + 1 end
    if brush.gx > brush.x then brush.gx = brush.gx - 1 end
    gpu.set(MENUX+1+brush.gx, 8, " -^--^- ")
  end
end
function drawLayerSelector()
  frame(MENUX, 16, WIDTH-2, 28, lang.layer)
  gpu.set(MENUX+13, 18, lang.level)
  gpu.set(MENUX+1, 23, lang.mainLevel)
end
function drawButtonsPanel()
  frame(MENUX, 28, WIDTH-2, 36, lang.control)
end

function mainScreen()
  term.clear()
  frame(1,1, WIDTH, HEIGHT, "{ Hologram Editor }")
  -- "холст"
  drawLayer()
  drawColorSelector()
  drawColorCursor(true)
  drawLayerSelector()
  drawButtonsPanel()
  buttonsDraw()
  textboxesDraw()
  -- "about" - коротко о создателях
  gpu.setForeground(infocolor)
  gpu.setBackground(graycolor)
  gpu.set(MENUX+3, HEIGHT-11, " Hologram Editor v0.60 Beta  ")
  gpu.setForeground(forecolor)
  gpu.set(MENUX+3, HEIGHT-10, "            * * *            ")
  gpu.set(MENUX+3, HEIGHT-9,  lang.developers)
  gpu.set(MENUX+3, HEIGHT-8,  "         NEO, Totoro         ")
  gpu.set(MENUX+3, HEIGHT-7,  "            * * *            ")
  gpu.set(MENUX+3, HEIGHT-6,  lang.contact)
  gpu.set(MENUX+3, HEIGHT-5,  "   computercraft.ru/forum    ")
  gpu.setBackground(backcolor)
  -- выход
  gpu.set(MENUX, HEIGHT-2, lang.quit)
end


-- =============================================== L A Y E R S =============================================== --
GRIDX = 3
GRIDY = 2
function drawLayer()
  drawGrid(GRIDX, GRIDY)
  -- вид сверху (y)
  if view == 0 then
    for x=1, HOLOW do
      for z=1, HOLOW do
        gn = get(x, ghost_layer, z)
        n = get(x, layer, z)
        if n == 0 and gn ~= 0 then
          gpu.setForeground(darkhexcolors[gn])
          gpu.set((GRIDX-2) + x*2, (GRIDY-1) + z, "░░")
        end
        if n ~= 0 then
          gpu.setForeground(hexcolortable[n])
          gpu.set((GRIDX-2) + x*2, (GRIDY-1) + z, "██")
        end
      end
    end
  -- вид спереди (z)
  elseif view == 1 then
    for x=1, HOLOW do
      for y=1, HOLOH do
        n = get(x, y, layer)
        gn = get(x, y, ghost_layer)
        if n == 0 and gn ~= 0 then
          gpu.setForeground(darkhexcolors[gn])
          gpu.set((GRIDX-2) + x*2, (GRIDY+HOLOH) - y, "░░")
        end
        if n ~= 0 then
          gpu.setForeground(hexcolortable[n])
          gpu.set((GRIDX-2) + x*2, (GRIDY+HOLOH) - y, "██")
        end
      end
    end
  -- вид сбоку (x)
  else
    for z=1, HOLOW do
      for y=1, HOLOH do
        gn = get(ghost_layer, y, z)
        n = get(layer, y, z)
        if n == 0 and gn ~= 0 then
          gpu.setForeground(darkhexcolors[gn])
          gpu.set((GRIDX+HOLOW*2) - z*2, (GRIDY+HOLOH) - y, "░░")
        end
        if n ~= 0 then
          gpu.setForeground(hexcolortable[n])
          gpu.set((GRIDX+HOLOW*2) - z*2, (GRIDY+HOLOH) - y, "██")
        end
      end
    end
  end
  gpu.setForeground(forecolor)
  -- for messages
  repaint = false
end
function fillLayer()
  for x=1, HOLOW do
    for z=1, HOLOW do
      set(x, layer, z, brush.color)
    end
  end
  drawLayer()
end
function clearLayer()
  for x=1, HOLOW do
    if holo[x] ~= nil then holo[x][layer] = nil end
  end
  drawLayer()
end


-- ============================================== B U T T O N S ============================================== --
Button = {}
Button.__index = Button
function Button.new(func, x, y, text, color, width)
  self = setmetatable({}, Button)

  self.form = '[ '
  if width == nil then width = 0
    else width = (width - unicode.len(text))-4 end
  for i=1, math.floor(width/2) do
    self.form = self.form.. ' '
  end
  self.form = self.form..text
  for i=1, math.ceil(width/2) do
    self.form = self.form.. ' '
  end
  self.form = self.form..' ]'

  self.func = func

  self.x = x; self.y = y
  self.color = color
  self.visible = true

  return self
end
function Button:draw(color)
  if self.visible then
    local color = color or self.color
    gpu.setBackground(color)
    if color > 0x888888 then gpu.setForeground(backcolor) end
    gpu.set(self.x, self.y, self.form)
    gpu.setBackground(backcolor)
    if color > 0x888888 then gpu.setForeground(forecolor) end
  end
end
function Button:click(x, y)
  if self.visible then
    if y == self.y then
      if x >= self.x and x < self.x+unicode.len(self.form) then
        self.func()
        self:draw(self.color/2)
        os.sleep(0.1)
        self:draw()
        return true
      end
    end
  end
  return false
end
buttons = {}
function buttonsNew(func, x, y, text, color, width)
  table.insert(buttons, Button.new(func, x, y, text, color, width))
end 
function buttonsDraw()
  for i=1, #buttons do
    buttons[i]:draw()
  end
end
function buttonsClick(x, y)
  for i=1, #buttons do
    buttons[i]:click(x, y)
  end
end

-- ================================ B U T T O N S   F U N C T I O N A L I T Y ================================ --
function exit() running = false end
function nextLayer()
  -- ограничения разные для разных видов/проекций
  local limit = HOLOH
  if view > 0 then limit = HOLOW end

  if layer < limit then 
    layer = layer + 1
    tb_layer:setValue(layer)
    tb_layer:draw(true)
    moveGhost()
    drawLayer()
  end
end
function prevLayer()
  if layer > 1 then 
    layer = layer - 1 
    tb_layer:setValue(layer)
    tb_layer:draw(true)
    moveGhost()
    drawLayer()
  end
end
function setLayer(value)
  local n = tonumber(value)
  local limit = HOLOH
  if view > 0 then limit = HOLOW end
  if n == nil or n < 1 or n > limit then return false end
  layer = n
  moveGhost()
  drawLayer()
  return true
end
function nextGhost()
  local limit = HOLOH
  if view > 0 then limit = HOLOW end
  
  if ghost_layer_below then
    ghost_layer_below = false
    if ghost_layer < limit then
      ghost_layer = layer + 1
    else ghost_layer = limit end
    drawLayer()
  else  
    if ghost_layer < limit then
      ghost_layer = ghost_layer + 1 
      drawLayer()
    end
  end
end
function prevGhost()
  if not ghost_layer_below then
    ghost_layer_below = true
    if layer > 1 then
      ghost_layer = layer - 1
    else ghost_layer = 1 end
    drawLayer()
  else
    if ghost_layer > 1 then
      ghost_layer = ghost_layer - 1
      drawLayer()
    end
  end
end
function setGhostLayer(value)
  local n = tonumber(value)
  local limit = HOLOH
  if view > 0 then limit = HOLOW end
  if n == nil or n < 1 or n > limit then return false end
  ghost_layer = n
  drawLayer()
  return true
end
function moveGhost()
  if ghost_layer_below then
    if layer > 1 then ghost_layer = layer - 1
    else ghost_layer = 1 end
  else
    local limit = HOLOH
    if view > 0 then limit = HOLOW end
    if layer < limit then ghost_layer = layer + 1
    else ghost_layer = limit end
  end
end

function setFilename(str)
  if str ~= nil and str ~= '' and unicode.len(str)<30 then 
    return true
  else
    return false
  end
end

function setHexColor(n, r, g, b)
  local hexcolor = rgb2hex(r,g,b)
  hexcolortable[n] = hexcolor
  darkhexcolors[n] = bit32.rshift(bit32.band(hexcolor, 0xfefefe), 1)
end
function rgb2hex(r,g,b)
  return r*65536+g*256+b
end
function changeRed(value) return changeColor(1, value) end
function changeGreen(value) return changeColor(2, value) end
function changeBlue(value) return changeColor(3, value) end
function changeColor(rgb, value)
  if value == nil then return false end
  n = tonumber(value)
  if n == nil or n < 0 or n > 255 then return false end
  -- сохраняем данные в таблицу
  colortable[brush.color][rgb] = n
  setHexColor(brush.color, colortable[brush.color][1],
                           colortable[brush.color][2],
                           colortable[brush.color][3])
  -- обновляем цвета на панельке
  for i=0, 3 do
    drawRect(MENUX+1+i*8, 5, hexcolortable[i])
  end
  return true
end

function moveSelector(num)
  brush.color = num
  tb_red:setValue(colortable[num][1]); tb_red:draw(true)
  tb_green:setValue(colortable[num][2]); tb_green:draw(true)
  tb_blue:setValue(colortable[num][3]); tb_blue:draw(true)
end

function setTopView() 
  view = 0 
  -- в виде сверху меньше слоев
  if layer > HOLOH then layer = HOLOH end
  drawLayer()
end
function setFrontView() view = 1; drawLayer() end
function setSideView() view = 2; drawLayer() end

function drawHologram()
  -- проверка на наличие проектора
  h = trytofind('hologram')
  if h ~= nil then
    local depth = h.maxDepth()
    -- очищаем
    h.clear()
    -- отправляем палитру
    if depth == 2 then
      for i=1, 3 do
        h.setPaletteColor(i, hexcolortable[i])
      end
    else
      h.setPaletteColor(1, hexcolortable[1])
    end
    -- отправляем массив
    for x=1, HOLOW do
      for y=1, HOLOH do
        for z=1, HOLOW do
          n = get(x,y,z)
          if n ~= 0 then
            if depth == 2 then
              h.set(x,y,z,n)
            else
              h.set(x,y,z,1)
            end
          end
        end
      end      
    end
  end
end

function newHologram()
  holo = {}
  drawLayer()
end

function saveHologram()
  local filename = tb_file:getValue()
  if filename ~= FILE_REQUEST then
    -- выводим предупреждение
    showMessage(lang.savingFile, lang.attention, goldcolor)
    -- добавляем фирменное расширение =)
    if string.sub(filename, -3) ~= '.3d' then
      filename = filename..'.3d'
    end
    -- сохраняем
    save(filename)
    -- выводим предупреждение
    showMessage(lang.complete, lang.attention, goldcolor)
    repaint = true
  end
end

function loadHologram()
  local filename = tb_file:getValue()
  if filename ~= FILE_REQUEST then
    -- выводим предупреждение
    showMessage(lang.loadingFile, lang.attention, goldcolor)
    -- добавляем фирменное расширение =)
    if string.sub(filename, -3) ~= '.3d' then
      filename = filename..'.3d'
    end
    -- загружаем
    load(filename)
    -- обновляем значения в текстбоксах
    tb_red:setValue(colortable[brush.color][1]); tb_red:draw(true)
    tb_green:setValue(colortable[brush.color][2]); tb_green:draw(true)
    tb_blue:setValue(colortable[brush.color][3]); tb_blue:draw(true)
    -- обновляем цвета на панельке
    for i=0, 3 do
      drawRect(MENUX+1+i*8, 5, hexcolortable[i])
    end
    -- обновляем слой
    drawLayer()
  end
end

-- ============================================ T E X T B O X E S ============================================ --
Textbox = {}
Textbox.__index = Textbox
function Textbox.new(func, x, y, value, width)
  self = setmetatable({}, Textbox)

  self.form = '>'
  if width == nil then width = 10 end
  for i=1, width-1 do
    self.form = self.form..' '
  end

  self.func = func
  self.value = tostring(value)

  self.x = x; self.y = y
  self.visible = true

  return self
end
function Textbox:draw(content)
  if self.visible then
    if content then gpu.setBackground(graycolor) end
    gpu.set(self.x, self.y, self.form)
    if content then gpu.set(self.x+2, self.y, self.value) end
    gpu.setBackground(backcolor)
  end
end
function Textbox:click(x, y)
  if self.visible then
    if y == self.y then
      if x >= self.x and x < self.x+unicode.len(self.form) then
        self:draw(false)
        term.setCursor(self.x+2, self.y)
        value = string.sub(term.read({self.value}), 1, -2)
        if self.func(value) then
          self.value = value
        end
        self:draw(true)
        return true
      end
    end
  end
  return false
end
function Textbox:setValue(value)
  self.value = tostring(value)
end
function Textbox:getValue()
  return self.value
end
textboxes = {}
function textboxesNew(func, x, y, value, width)
  textbox = Textbox.new(func, x, y, value, width)
  table.insert(textboxes, textbox)
  return textbox
end 
function textboxesDraw()
  for i=1, #textboxes do
    textboxes[i]:draw(true)
  end
end
function textboxesClick(x, y)
  for i=1, #textboxes do
    textboxes[i]:click(x, y)
  end
end


-- ============================================= M E S S A G E S ============================================= --
repaint = false
function showMessage(text, caption, color)
  local x = WIDTH/2 - unicode.len(text)/2 - 4
  local y = HEIGHT/2 - 2
  gpu.fill(x, y, unicode.len(text)+8, 5, ' ')
  frame(x, y, x+unicode.len(text)+7, y+4, caption)
  gpu.setForeground(color)
  gpu.set(x+4,y+2, text)
  gpu.setForeground(forecolor)
end


-- =========================================== M A I N   C Y C L E =========================================== --
-- инициализация
colortable = {{255, 0, 0}, {0, 255, 0}, {0, 102, 255}}
colortable[0] = {0, 0, 0}
hexcolortable = {}
darkhexcolors = {}
for i=0,3 do setHexColor(i, colortable[i][1], colortable[i][2], colortable[i][3]) end
brush = {color = 1, x = 8, gx = 8}
ghost_layer = 1
ghost_layer_below = true
layer = 1
view = 0
running = true

buttonsNew(exit, WIDTH-BUTTONW-2, HEIGHT-2, lang.exit, errorcolor, BUTTONW)
buttonsNew(drawLayer, MENUX+10, 14, lang.refresh, goldcolor, BUTTONW)
buttonsNew(prevLayer, MENUX+1, 19, '-', infocolor, 5)
buttonsNew(nextLayer, MENUX+7, 19, '+', infocolor, 5)
buttonsNew(setTopView, MENUX+1, 21, lang.fromUp, infocolor, 10)
buttonsNew(setFrontView, MENUX+12, 21, lang.fromFront, infocolor, 10)
buttonsNew(setSideView, MENUX+24, 21, lang.fromSide, infocolor, 9)

buttonsNew(prevGhost, MENUX+1, 24, lang.lower, infocolor, 6)
buttonsNew(nextGhost, MENUX+10, 24, lang.upper, infocolor, 6)

buttonsNew(clearLayer, MENUX+1, 26, lang.clear, infocolor, BUTTONW)
buttonsNew(fillLayer, MENUX+2+BUTTONW, 26, lang.fill, infocolor, BUTTONW)

buttonsNew(drawHologram, MENUX+8, 30, lang.toProjector, goldcolor, 16)
buttonsNew(saveHologram, MENUX+1, 33, lang.save, helpcolor, BUTTONW)
buttonsNew(loadHologram, MENUX+8+BUTTONW, 33, lang.load, infocolor, BUTTONW)
buttonsNew(newHologram, MENUX+1, 35, lang.new, infocolor, BUTTONW)

tb_red = textboxesNew(changeRed, MENUX+5, 10, '255', WIDTH-MENUX-7)
tb_green = textboxesNew(changeGreen, MENUX+5, 11, '0', WIDTH-MENUX-7)
tb_blue = textboxesNew(changeBlue, MENUX+5, 12, '0', WIDTH-MENUX-7)
tb_layer = textboxesNew(setLayer, MENUX+13, 19, '1', WIDTH-MENUX-15)
tb_ghostlayer = textboxesNew(setGhostLayer, MENUX+19, 24, ' ', WIDTH-MENUX-21)
FILE_REQUEST = lang.enterFileName
tb_file = textboxesNew(setFilename, MENUX+1, 32, FILE_REQUEST, WIDTH-MENUX-3)
mainScreen()

while running do
  if brush.x ~= brush.gx then name, add, x, y, b = event.pull(0.02)
  else name, add, x, y, b = event.pull(1.0) end

  if name == 'key_down' then 
    -- если нажата 'Q' - выходим
    if y == 16 then break 
    elseif y == 41 then
      moveSelector(0)
    elseif y>=2 and y<=4 then
      moveSelector(y-1)
    elseif y == 211 then
      clearLayer()
    end
  elseif name == 'touch' then
    -- проверка GUI
    buttonsClick(x, y)
    textboxesClick(x, y)
    -- выбор цвета
    if x>MENUX+1 and x<MENUX+37 then
      if y>4 and y<8 then
        moveSelector(math.floor((x-MENUX-1)/8))
      end
    end
  end
  if name == 'touch' or name == 'drag' then
    -- "рисование"
    local limit = HOLOW
    if view > 0 then limit = HOLOH end
    if x >= GRIDX and x < GRIDX+HOLOW*2 then
      if y >= GRIDY and y < GRIDY+limit then
        -- перерисуем, если на экране был мессейдж
        if repaint then drawLayer() end
        -- рассчет клика
        if view == 0 then
          dx = math.floor((x-GRIDX)/2)+1; gx = dx
          dy = layer; gy = ghost_layer
          dz = y-GRIDY+1; gz = dz
        elseif view == 1 then
          dx = math.floor((x-GRIDX)/2)+1; gx = dx
          dy = HOLOH - (y-GRIDY); gy = dy
          dz = layer; gz = ghost_layer
        else
          dx = layer; gx = ghost_layer
          dy = HOLOH - (y-GRIDY); gy = dy
          dz = HOLOW - math.floor((x-GRIDX)/2); gz = dz
        end
        if b == 0 and brush.color ~= 0 then
          set(dx, dy, dz, brush.color)
          gpu.setForeground(hexcolortable[brush.color])
          gpu.set(x-(x-GRIDX)%2, y, "██")
        else
          set(dx, dy, dz, 0)
          gpu.setForeground(darkhexcolors[get(gx,gy,gz)])
          gpu.set(x-(x-GRIDX)%2, y, "░░")
        end
        gpu.setForeground(forecolor)
      end
    end
  end

  drawColorCursor()
end

-- завершение
term.clear()
gpu.setResolution(OLDWIDTH, OLDHEIGHT)
gpu.setForeground(0xFFFFFF)
gpu.setBackground(0x000000)
