#!/usr/bin/env ruby

=begin rdoc

This program is copyright 2014 by Vincent Fourmond.

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, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
=end


# This program is a wrapper around ctioga2 to make it easy to make
# movies.

require 'open3'
require 'fileutils'
require 'optparse'

require 'shellwords'

# Path to ctioga2 executable
ct2 = "ctioga2"


tmpdir = "tmp"


# The class in charge of running ffmpeg
class EncodingJob

  # Target file
  attr_accessor :target

  # Bitrate
  attr_accessor :bitrate

  # Video codec
  attr_accessor :codec

  # Extra arguments
  attr_accessor :extra_args

  # Writes the given frame for encoding
  def write_frame(data)
    @encoder.write(data)
  end

  def add_args(args)
    @extra_args ||= []
    @extra_args += args
  end

  # Start job
  def start_job(size)
    ffmpeg_args = ["ffmpeg", '-y', "-f", "rawvideo", 
                   "-r", "25", "-s", size, "-i", "-"]
    if @bitrate
      ffmpeg_args << "-b" << @bitrate
    end
    if @codec
      ffmpeg_args << "-vcodec" << @codec
    end

    ffmpeg_args += @extra_args if @extra_args
    ffmpeg_args << @target
    p ffmpeg_args
    @encoder = IO::popen(ffmpeg_args, "wb")
  end

  def close()
    @encoder.close
  end
end


DimensionConversion = {
  "pt" => (72.0/72.27),
  "bp" => 1.0,
  "in" => 72.0,
  "cm" => (72.0/2.54),
  "mm" => (72.0/25.4),
}

def dim_to_points(dim)
  if dim =~ /^\s*(\d+(\.\d*)?)\s*(pt|bp|cm|in|mm)\s*$/i
    return $1.to_f * DimensionConversion[$3.downcase]
  else
    raise "Invalid dimension: #{dim}"
  end
end

# Converts the given "real-size" resolution into postscript points
def page_size_to_points(spec)
  if spec =~ /(.*)x(.*)/i
    return [dim_to_points($1), dim_to_points($2)]
  else
    raise "Invalid page size: #{spec}"
  end
end

# @todo Build a movie from a list of PDF files. Maybe less error
# checking than directly.

# The target resolution
res = [600,600]

# The target page size (in bp)
ct2_size = nil

# The corresponding number of points

# The conversion factor (between points and inches)
conv = 250.0

# The oversampling factor (to get something smooth in the end)
oversampling = 2

# Whether we use pdftoppm or not for the conversion. Much faster than
# convert
use_pdftoppm = false

# Whether we keep all intermediate PDF files, or we reuse the same
# file over and over again.
store_all = true


encoders = [ EncodingJob.new ]
cur_enc = encoders.first


opts = OptionParser.new do |opts|
  opts.banner = "Usage: #$0 [options] file.ct2 arguments..."

  ##################################################
  # Encoding-related options
  opts.on("-t", "--target FILE", "Target video file") do |t|
    if cur_enc.target
      cur_enc = EncodingJob.new
      encoders << cur_enc
    end
    cur_enc.target = t
  end

  opts.on("-b", "--bitrate RATE", "Bitrate (indicative)") do |v|
    cur_enc.bitrate = v
  end

  opts.on("", "--codec CODEC", "Target codec") do |v|
    cur_enc.codec = v
  end

  opts.on("", "--ffmpeg-args ARGS", "Extra ffmpeg args") do |v|
     cur_enc.add_args(Shellwords.split(v))
  end


  ##################################################
  # 

  opts.on("", "--dir DIR", "Temporary directory for storage") do |t|
    tmpdir = t
  end

  opts.on("", "--version", "Prints version string") do 
    puts "0.1"
  end

  opts.on("-p", "--[no-]pdftoppm", "Whether or not to use pdftoppm") do |t|
    use_pdftoppm = t
  end

  opts.on("-r", "--page-size SIZE", 
          "Set ctioga2 page size (in TeX dimensions)") do |v|
    ct2_size = page_size_to_points(v)
    p ct2_size
  end

  opts.on("", "--resolution RES",
          "Set target resolution (overridden to some extent by page-size)") do |r|
    r =~ /(\d+)x(\d+)/
    res = [$1.to_f, $2.to_f]
  end

  opts.on("", "--[no-]store", "To store all or not..") do |v|
    store_all = v
  end


end

opts.parse!(ARGV)

# First, we choose the target page size and resolution. 

if ct2_size
  ct2_page_size = ct2_size.map { |x| "#{x}bp" }.join("x") 
  # maintain aspect ratio
  res[1] = res[0] * ct2_size[1]/ct2_size[0]
  conv = res[0]/ct2_size[0] * 72.27
else
  ct2_page_size = res.map { |x| "#{x/conv}in"}.join("x")
end

size = res.map { |x| "#{x.to_i}"}.join("x")

puts "Producing #{ct2_page_size} PDF and converting to #{size} for the video"


file = ARGV.shift
cur_enc.target ||= file.sub(/(\.ct2)?$/, ".avi")
args = []

for a in ARGV
  # Expansion !
  if a =~ /^(.*)\.\.(.*):(\d+)\s*$/
    s = $1.to_f
    e = $2.to_f
    nb = $3.to_i
    nb.times do |i|
      args << "#{s + (e-s)*i/(nb-1.0)}"
    end
  else
    args << a
  end
end


FileUtils::mkpath(tmpdir)

# Now, we compute the ctioga2 real size 


# @todo Use other encoding programs !

# Start all encoders
for enc in encoders
  enc.start_job(size)
end

format = if store_all 
           "#{tmpdir}/file-%04d"
         else
           "#{tmpdir}/file"
         end


index = 0
for f in args
  name = format % index

  ct2_cmdline = [ct2, 
                 "--set", "arg", f, 
                 "--set", "index", "#{index}", 
                "-f", file, "--name", name, "-r", ct2_page_size]
  puts "Running: #{ct2_cmdline.join(" ")}"
  system(*ct2_cmdline)

  b = nil

  if use_pdftoppm
    b1, s = Open3.capture2(
                          "pdftoppm",
                          "-r",
                          "#{(conv*oversampling).to_i}",
                           "#{name}.pdf",
                          :stdin_data=>"", :binmode=>true)
    b, s = Open3.capture2("convert",
                          "PPM:-",
                          "-resize", "#{size}!",
                          "-depth", "8", "YUV:-",
                          :stdin_data=>b1, :binmode=>true)
  else
    b, s = Open3.capture2("convert",
                          "-density", "#{(conv*oversampling).to_i}",
                          "#{name}.pdf",
                          "-alpha", "Remove",
                          "-resize", "#{size}!",
                          "-depth", "8", "YUV:-",
                          :stdin_data=>"", :binmode=>true)
  end
  for enc in encoders
    enc.write_frame(b)
  end
  index += 1
end

for enc in encoders
  enc.close
end
