# PDF-O-Matic is copyright (c) 2009, Brett Neumeier.
#
# PDF-O-Matic may be used under the terms of the GPL.  See
# COPYING.GPL, provided in this distribution, for full details.
#
# DOJ and CompNet Design, Inc., are granted a perpetual license to
# use, extend, modify, and redistribute the PDF-O-Matic software as
# they wish, without restriction.

# This is the base class for character sheet generators that work by
# writing additional content into an existing character sheet PDF.

# Load-path setup. Assume everything is either available as a rubygem
# or in a vendor lib directory.
lib_path = File.expand_path(File.join(File.dirname(__FILE__)))
vendor_lib_dirs = []
begin
  require 'rubygems'
rescue
  vendor_dir = File.join(File.dirname(lib_path), 'vendor')
  vendor_lib_dirs = Dir.glob(File.join(vendor_dir, '*', 'lib'))
end
includes = [ 'lib', lib_path, vendor_lib_dirs ].flatten
includes.each { |dir| $LOAD_PATH.unshift(dir) unless $LOAD_PATH.include?(dir) }

# Library dependencies:
require 'stringio'
require 'yaml'
require 'date'
require 'prawn/core'

# PDF-O-Matic classes:
require 'pdf_helpers'
require 'skill_box'
require 'power_box'
require 'disad_box'
require 'maneuver_box'

class TemplateOverlay
  include PdfHelpers

  def initialize(filename, template, resource_dir = nil)
    (ENV['PDF_RESOURCE_DIR'] = resource_dir) if resource_dir

    @config = YAML.load_file(resource('config.yml'))
    @char = YAML.load(yamlize(filename))
    @pdf = Prawn::Document.new(:template => resource("#{template}.pdf"),
                               :page_size => 'LETTER',
                               :margin => [0,0,0,0])
    @font = @config['fillin_font'] 
    @primary_color = @config['primary_color'].split(',').map { |x| x.to_f }
    @secondary_color = @config['secondary_color'].split(',').map { |x| x.to_f }
  end
  attr_reader :char, :pdf

  # It's very difficult to produce valid YAML from HeroDesigner,
  # because some of the HD-generated strings have quote and
  # apostrophe characters that can't be reliably escaped, and the
  # HD replacement strings affect both dynamic and template
  # content.  So just fix the input stream up before YAML-parsing
  # it.
  def yamlize(filename)
    raw = File.read(filename)
    yaml_io = StringIO.new('', 'w')
    raw.lines.each { |line|
      line.chomp!
      next if line =~ /<!--[A-Z_]*-->/
      line.gsub!('"','\"')
      if need_to_yaml_escape(line)
        line.sub!(/: /, ': "')
        line.sub!(/$/, '"')
      end
      yaml_io.puts(line)
    }
    yaml_io.close
    yaml_io.string
  end

  def need_to_yaml_escape(line)
    line =~ /^[ -]*[a-z_]+: .*[+\['":]/
  end

  def bold_font
    @config['bold_font']
  end

  def medium_font
    @config['medium_font']
  end

  def wide_bold_font
    @config['wide_bold_font']
  end

  def resource_dir
    if ENV['PDF_RESOURCE_DIR']
      ENV['PDF_RESOURCE_DIR']
    else
      File.expand_path(
          File.join(
              File.dirname(File.dirname(__FILE__)), 
              'resources'))
    end
  end

  def resource(filename)
    File.join(resource_dir, filename)
  end

  def generate(output)
    primary_color
    set_font(@font)
    @sb = SkillBox.new(@pdf).add_all(@char)
    @pb = PowerBox.new(@pdf).add_all(@char)
    @db = DisadBox.new(@pdf).add_all(@char)
    @mb = ManeuverBox.new(@pdf).add_all(@char)
    first_page_blocks
    pdf.go_to_page(1)
    primary_color
    second_page_blocks
    continuation_pages if overflow_items_remain
    pdf.render_file(output)
  end

  def first_page_blocks
    header_block
    characteristic_block
    experience_block
    defense_block
    movement_block
    pdf.font_size(7.5) { combat_maneuvers_block }
    additional_first_page_blocks
  end

  def second_page_blocks
    character_and_campaign_info
    pdf.font_size(8) { skills; powers; disads }
  end

  def render_header_block(font_size, leading, xpos, ypos)
    pdf.font_size(font_size) {
      text_left_at(char['character_name'], xpos, ypos)
      text_left_at(char['alt_identities'], xpos, ypos - (leading + font_size))
      text_left_at(char['player_name'], xpos, ypos - 2 * (leading + font_size))
    }
  end

  # If powers, skills, or disadvantages remain after filling in
  # the initial character sheet, print them on additional pages.
  # Since there probably aren't that many of them, just use one
  # column the width of the page.
  def continuation_pages
    pdf.create_stamp('Logo') do
      pdf.add_content(File.read(resource(logo_file)))
    end
    while overflow_items_remain
      pdf.start_new_page
      continuation_page_header
      @pdf.font_size(10)
      ypos = continuation_page_item_position
      ypos = skills_continued(ypos) unless @sb.items.empty?
      ypos = powers_continued(ypos) unless @pb.items.empty?
      ypos = disadvantages_continued(ypos) unless @db.items.empty?
    end
  end

  def overflow_items_remain
    [ @sb, @pb, @db ].any? { |box| box.items.size > 0 }
  end

  def add_characteristic_skill_levels
    return unless Enumerable ===  char['skills']
    levels = char['skills'].select { |s|
      s['xmlid'] == 'SKILL_LEVELS' && s['option_id'] == 'CHARACTERISTIC'
    }
    levels.each { |l|
      md = /.* ([A-Z]*) .*/.match(l['option_alias'])
      c = md ? md[1].downcase : ''
      if char["#{c}_roll"]
        vals = char["#{c}_roll"].scan(/([0-9]+)-/).flatten.map { |v| v.to_i }
        new_vals = vals.map { |v| v + l['levels'] }
        roll_str = new_vals.map { |v| "#{v.to_s}-" }.join('/')
        char["#{c}_roll"] = roll_str
      end
    }
  end

  def print_phases
    if char['phases'] =~ Regexp.new('/')
      primary_str, secondary_str = char['phases'].split('/')
    else
      primary_str, secondary_str = char['phases'], ""
    end
    primary = primary_str.scan(/[0-9]+/).map { |p| p.to_i }
    secondary = secondary_str.scan(/[0-9]+/).map { |p| p.to_i }
    1.upto(12) { |phase|
      if primary.include?(phase)
        circle_phase(phase, 4.5)
      end
      if secondary.include?(phase)
        radius = primary.include?(phase) ? 2.5 : 4.5
        with_secondary_color { circle_phase(phase, radius) }
      end
    }
  end

  def circle_phase(phase, radius)
    r = phase < 10 ? radius : (radius + 1)
    pdf.circle_at([phase_x_positions[phase], phase_y_position], :radius => r)
    pdf.stroke
  end

  def horiz_to_vertical(horiz_leap)
    primary, secondary = horiz_leap.split('/')
    vert_leap = halve_value(primary)
    (vert_leap = vert_leap + '/' + halve_value(secondary)) if secondary
    vert_leap
  end
  
  def halve_value(move)
    match = /(\d+)(.)/.match(move)
    val = (match[1].to_i / 2).to_s
    unit = match[2]
    "#{val}#{unit}"
  end

  def movement_powers_to_show
    pows = []
    [ 'flight', 'teleportation', 'gliding', 'tunneling', 'swinging' ].each { |p|
      pows << p if char[p] 
    }
    pows[0..2]
  end

  # Skills, powers, disads and martial art maneuvers are all handled
  # the same way: The regions on the default character sheet have
  # fairly wide linespacing by default (12 or 16 points), but we're
  # using a smaller font (8 point) for the text.  If everything fits
  # on the existing lines, that's great, just draw on them.  If not,
  # white-out the pre-drawn lines and convert to the natural
  # line-spacing of the font being used.
  #
  # text_width is the width of the main "text" column, if there is
  # one. If there isn't one, give it a big number.
  def render_item_list(top_line_pos, text_width, itembox, regions,
                       provided_spacing = 16)
    natural_spacing = pdf.font_size
    provided_spacing_top = top_line_pos
    natural_spacing_top = top_line_pos + (provided_spacing - natural_spacing)

    rows_needed = itembox.total_height(text_width)
    provided_rows = regions.inject(0) { |sum, r| sum + r.provided_rows }

    if rows_needed <= provided_rows
      linedrop = provided_spacing
    else
      linedrop = natural_spacing
    end

    regions.each { |r|
      if linedrop == provided_spacing
        ypos = provided_spacing_top
        rows = r.provided_rows
      else
        r.whiteout(self)
        ypos = natural_spacing_top
        rows = (r.provided_rows * provided_spacing / natural_spacing)
      end
      items = itembox.pull_items_for_region(rows, text_width)
      items.each { |item| ypos = yield(r.xpos, ypos, linedrop, item) }
    }
  end


  # Little struct class used by the item-renderer.
  # provided_rows is the number of rows provided on the char sheet
  # xpos is the x-axis rendering point for the leftmost column
  # when whiting-out provided lines, white-out from (x1, y1) to (x2, y2)
  class ItemListRegion
    def initialize(provided_rows, xpos, x1, y1, x2, y2)
      @provided_rows = provided_rows
      @xpos = xpos
      @x1 = x1
      @y1 = y1
      @x2 = x2
      @y2 = y2
    end
    attr_reader :provided_rows, :xpos

    def whiteout(pdf_helper)
      pdf_helper.whiteout(@x1, @y1, @x2, @y2)
    end
  end

  def skill_line(item, xpos, ypos, linedrop, col_widths)
    return ypos if ItemBox::SeparatorItem === item
    text_centered_at(item.cost, xpos, ypos, col_widths[0])
    xpos += col_widths[0] + 2
    ypos = wrapped_text_at(item.text, xpos, ypos, linedrop, col_widths[1])
    xpos += col_widths[1] + 2
    text_centered_at(item.roll, xpos, ypos, col_widths[2])
    ypos
  end

  def power_line(item, xpos, ypos, linedrop, col_widths)
    text_centered_at(item.cost, xpos, ypos, col_widths[0])
    xpos += col_widths[0]
    if item.text && item.text.strip != ''
      name_height = height_of_text(item.name, col_widths[1])
      text_height = height_of_text(item.text, col_widths[2])
      if name_height <= text_height
        wrapped_text_at(item.name, xpos, ypos, linedrop, col_widths[1])
      else
        text_trimmed_at(item.name, xpos, ypos, col_widths[1])
      end
      xpos += col_widths[1] + 2
      ypos = wrapped_text_at(item.text, xpos, ypos, linedrop, col_widths[2])
      xpos += col_widths[2] + 2
    else
      colspan = col_widths[1] + col_widths[2]
      text_trimmed_at(item.name, xpos, ypos, colspan)
      xpos += colspan + 4
    end
    xpos += col_widths[3] / 2
    text_centered_at(item.endcost, xpos, ypos, col_widths[3])
    ypos
  end

  def disadvantage_line(item, xpos, ypos, linedrop, col_widths)
    text_centered_at(item.cost, xpos, ypos, col_widths[0])
    xpos += col_widths[0] + 2
    ypos = wrapped_text_at(item.text, xpos, ypos, linedrop, col_widths[1])
    xpos += col_widths[1] + 2
    text_centered_at(item.roll, xpos, ypos, 20)
    ypos
  end

  def set_font(filename)
    pdf.font(resource(filename), :size => 12)
    unless glyphs[188] && glyphs[189] && glyphs[190]
      raise "Font #{pdf.font.name} does not contain all fraction characters."
    end
  end

end
