#!/usr/bin/env ruby
#
# pcx2dat.rb -- Converts all the things.
#
# Copyright (C) 2013 trap15 <trap15@raidenii.net>
# All Rights Reserved.
#

$curpal = Array.new(16, 0xFF)
$curpalf = 0

$curvram = Array.new(32*96, 0xFF)
$newvram = Array.new(32*96, 0)

$palmap = Array.new(16, 0)

$curbank = 0
$bankfill = 0x10

$framespeed = 6

# 0 for speed
# 1 for size
OPTIMIZE_METH = 0

class Pcx
  attr_accessor :w, :h, :pal, :data
  def initialize(fname)
    @f = File.open(fname, "rb")
    @pal = Array.new(16, Array.new(3, 0))
    read()
    close()
  end

  def close
    @f.close
  end

  def read
    pcxhead = @f.read(16).unpack("CCCCS<S<S<S<S<S<")
    pal16 = @f.read(48).unpack("C"*3*16)
    @bpp = 4
    @f.read(64) # The rest of the header doesn't matter

    # Parse the header
    if(pcxhead[0] != 0x0A)
      raise 'Bad PCX file: ID'
    end
    if(pcxhead[1] != 5)
      raise 'Bad PCX file: only v3.0 supported'
    end
    if(pcxhead[2] != 1)
      raise 'Bad PCX file: encoding'
    end
    @bpp = pcxhead[3]
    unless @bpp == 8
      raise 'Bad PCX file: BPP unsupported'
    end
    @w = (pcxhead[6] - pcxhead[4]) + 1
    @h = (pcxhead[7] - pcxhead[5]) + 1

    @data = Array.new(@w * @h, 0)
    readData
    if @bpp == 8
      read8bppPal
    end
  end

  def readData
    ptr = 0
    for y in 0...@h
      x = 0
      while x < @w
        byte = @f.read(1).unpack("C")[0]
        if ((byte & 0xC0) == 0xC0)
          runcnt = byte & 0x3F
          runval = @f.read(1).unpack("C")[0]
        else
          runcnt = 1
          runval = byte
        end
        for i in 0...runcnt
          data[ptr+i] = runval & 0xF
        end
        ptr += runcnt
        x += runcnt
      end
    end
  end

  def read8bppPal
    @f.seek(-769, IO::SEEK_END)
    return unless @f.read(1).unpack("C")[0] == 0xC
    for c in 0...16
      @pal[c] = @f.read(3).unpack("CCC")
    end
  end

  def to_s
    ("Width:        %d\n" % (@w*16))+
    ("Height:       %d\n" % (@h*16))
  end
end

def add_to_bank(size)
  size *= 1
  $bankfill += size
  if $bankfill > 0x3FFE
    $bankfill = size
    $curbank += 1
    $ofp.printf "	.db	$C0, $%02X\n", $curbank+1
    $ofp.printf "\n\n"
    $ofp.printf "	.bank	%d	slot 2\n", $curbank + 1
    $ofp.printf "	.org	$0\n"
    $ofp.printf "	.orga	$8000\n"
  end
end

def conv_pal(pal, opal=nil, palf=0)
  if opal == nil
    opal = Array.new(16, 0x00)
  end
  for c in 0 ... 16
    r = pal[c][0]
    g = pal[c][1]
    b = pal[c][2]
    r += 0x20 if r < 0xC0; r >>= 6
    g += 0x20 if g < 0xC0; g >>= 6
    b += 0x20 if b < 0xC0; b >>= 6
    col = (r << 0) | (g << 2) | (b << 4)
    $palmap[c] = -1
    if palf != 0
      for cc in 0 ... palf
        if opal[cc] == col
          $palmap[c] = cc
        end
      end
    end
    if $palmap[c] == -1
      opal[palf] = col
      $palmap[c] = palf
      palf += 1
    end
  end

  return opal, palf
end

def handle_pal_match(img, npal, npalf)
  samepal = true

  for oc in 0 ... 16
    new = npal[oc]
    oldfound = false
    for nc in 0 ... 16
      old = $curpal[nc]
      if old == new
        oldfound = true
      end
    end
    if oldfound == false
      samepal = false
    end
  end

  if samepal == true
    npal, npalf = conv_pal(img.pal, $curpal, $curpalf)
    return
  end

  add_to_bank(3 + 16)
  $ofp.print "	.db	$80|$00\n"
  $ofp.print "		.db	$00\n"
  $ofp.print "		.db	-$10*2\n"
  $ofp.print "		.db	"
  for i in 0...16
    $ofp.print ", " unless i == 0
    $ofp.printf "$%02X", npal[i]
    $curpal[i] = npal[i]
    $curpalf = npalf
  end
  $ofp.print "\n"
end

def write_keyframe()
  add_to_bank(1 + 32*$height)
  $ofp.printf "	.db	$40|$%02X", $framespeed
  for l in 0 ... 32*$height
    $curvram[l] = $newvram[l]
  end

  for l in 0 ... 32*$height
    if (l & 15) == 0
      $ofp.print "\n		.db	"
    else
      $ofp.print ", "
    end
    $ofp.printf "$%02X", $curvram[l]
  end
  $ofp.print "\n"
end

def check_vram_diff()
  cnt = 0
  for i in 0 ... $height*32
    if $curvram[i] != $newvram[i]
      cnt += 1
    end
  end
  return cnt
end

def find_first_vram_diff()
  for y in 0 ... $height
    for x in 0 ... 32
      addr = x+(y*32)
      if y < 24
        vram = (x+( y    *32))*2 + 0x7000
      elsif y < 48
        vram = (x+((y-24)*32))*2 + 0x7800
      elsif y < 72
        vram = (x+((y-48)*32))*2 + 0x6000
      elsif y < 96
        vram = (x+((y-72)*32))*2 + 0x6800
      end
      if $curvram[addr] != $newvram[addr]
        i = addr
        i += 1 while $curvram[i] != $newvram[i]
        return addr, vram, i - addr
      end
    end
  end
end

def write_deltaframe_chunk()
  offset, vram, len = find_first_vram_diff()
  add_to_bank(7 + len)
  $ofp.printf "	.db	$00|$00\n"
  $ofp.printf "		.dw	$%04X\n", vram
  $ofp.printf "		.dw	-$%04X*4\n", len
  $ofp.printf "		.dw	CADDR+2"
  for i in 0 ... len
    if (i & 15) == 0
      $ofp.print "\n		.db	"
    else
      $ofp.print ", "
    end
    $curvram[offset+i] = $newvram[offset+i]
    $ofp.printf "$%02X", $newvram[offset+i]
  end
  $ofp.printf "\n"
end

def write_deltaframe()
  while check_vram_diff() != 0
    write_deltaframe_chunk()
  end
  add_to_bank(3)
  $ofp.printf "	.db	$80|$%02X, 0, 0\n", $framespeed
end

def calc_complexity(new, old)
  if new[0] != old[0] && old[0] == 0xFF
    return 99999999999
  end
  data = 0
  count = 0
  addr = 0
  while addr < 32*$height
    if $curvram[addr] != $newvram[addr]
      i = addr
      i += 1 while $curvram[i] != $newvram[i]
      data += i-addr
      count += 1
      addr = i
    end
    addr += 1
  end

  if OPTIMIZE_METH == 0 # Optimize for speed
    cpl = (count * 200 / 27) + data
  else # Optimize for size
    cpl = (count * 7) + data
  end

  printf "Complex: %04X\n", cpl
  return cpl
end

def handle_vram(img)
  for y in 0...$height
    for x in 0...32
      if $height == 48
        ry = y * 2
        ry -= 24*2 if y >= 24
        ry += 1 unless y >= 24
      elsif $height == 96
        ry = y * 4
        if y < 24
          ry += 3
        elsif y < 48
          ry -= 24*4
          ry += 0
        elsif y < 72
          ry -= 48*4
          ry += 1
        elsif y < 96
          ry -= 72*4
          ry += 2
        end
      end

      addr = (x*2) + (ry*64)
      pxl = $palmap[img.data[addr+0]]
      pxr = $palmap[img.data[addr+1]]
      $newvram[x+(y*32)] = (pxr << 4) | pxl
    end
  end

  cpx = calc_complexity($newvram, $curvram)

  if cpx < $complex_limit
    write_deltaframe()
  else
    write_keyframe()
  end
end

def do_file(fn)
  if !File.file?(fn)
    return
  end
  printf "Reading file [%s]\n", fn

  img = Pcx.new(fn)

  $ofp.printf "; %s\n", fn
  pal, palf = conv_pal(img.pal)
  handle_pal_match(img, pal, palf)

  handle_vram(img)
end

def xtoi(str)
  if str[0,2] == "0x"
    return str.hex
  else
    return str.to_i
  end
end

unless ARGV.length != 6
  print "Incorrect arguments. Usage:\n"
  print "  #{$0} outfile height start end template\n\n"
  exit
end

$out = ARGV[0]
$height = Integer(ARGV[1])
$start = Integer(ARGV[2])
$end = Integer(ARGV[3])
$intemp = ARGV[4]


if OPTIMIZE_METH == 0 # Optimize for speed
  $complex_limit = 32*$height
else # Optimize for size
  $complex_limit = 1+32*$height
end

$ofp = File.open($out, "wb+")
for i in $start..$end
  fn = sprintf $intemp, i
  do_file(fn)
end

$ofp.print "	.db	$C0, $01\n\n"

$ofp.close


