#!/usr/bin/ruby
# Ronan Le Hy, 2008
#     Ekee : a LaTeX equation editor.
#     Copyright (C) 2008 Ronan Le Hy
#
#     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 Street, Fifth Floor, Boston, MA 02110-1301 USA.

require 'optparse'
require 'yaml'
require 'Qt4'
# Qt.debug_level = Qt::DebugLevel::High

require 'fileutils'
require 'tempfile'
require 'ftools'
require 'uri'

$NAME = 'Ekee'
$VERSION = '2.0.2'

$verbose = false

def dump_object(o, indent = 0)
  puts((" " * indent) + o.to_s + (begin " enabled: " + o.enabled.to_s rescue "" end))
  o.children.each do |c|
    dump_object(c, indent + 2)
  end
end

def log_exception(e)
  if $verbose
    puts "Caught exception: " + e
    puts "Backtrace:", e.backtrace
  end
end

def verbose_message(m)
  if $verbose
    puts "(II) " + m
  end
end

def nilor(v, d)
  if v.nil?
    return d
  else
    return v
  end
end

module EqePath
  Home = ENV['HOME']
  ConfigDirectory = File.join(Home, '.eqe')
  LatexTemplate = File.join(ConfigDirectory, 'template.tt.tex')
  Library = File.join(ConfigDirectory, 'library')
end

module Template
  def Template.consume(input, t)
    input[0...t.size] = ''
  end

  def Template.parse_text(input)
    if 0 == (input =~ /^((?:[^\[]|\[[^%])+)/)
      consume(input, $1)
      return $1
    else
      return nil
    end
  end

  def Template.parse_tag(input, no_consume = false)
    if 0 == (input =~ /^(\[%((?:[^%]|%[^\]])+)%\])/)
      consumable = $1
      ret = $2
      remove_endl = [ret.sub!(/^-/, ''), ret.sub!(/-$/, '')]
      ret = ret.sub(/\s+$/, '').sub(/^\s+/, '')
      ret = [ret, *remove_endl] + [consumable]
      return ret
    else
      return nil
    end
  end

  def Template.dispatch_tag(t, input, buf, replace)
    if 0 == (t[0] =~ /^IF\s+(.*)/)
      if_buf = ''
      parse_main(input, if_buf, replace)
      buf << if_buf if replace[$1]
      t = parse_tag(input) # must be an END tag
      t[0] == 'END' or raise "parse error: expected [% END %] tag matching '#{t}' at #{input[0..20]}..."
      consume(input, t[3])
      buf[-1] = '' if t[1] and buf[-1] == "\n"[0]
      parse_main(input, buf, replace)
      buf[0] = '' if t[2] and buf[0] == "\n"[0]
    elsif t[0] =~ /(\S+)/
      buf << replace[$1].to_s
      parse_main(input, buf, replace)
    else
      raise "parse error: cannot parse tag '#{t}'"
    end
  end

  def Template.parse_main(input, buf, replace)
    if t = parse_text(input)
      buf << t
      parse_main(input, buf, replace)
    elsif t = parse_tag(input)
      return nil if t[0] == 'END'
      consume(input, t[3])
      buf[-1] = '' if t[1] and buf[-1] == "\n"[0]
      dispatch_tag(t, input, buf, replace)
      buf[0] = '' if t[2] and buf[0] == "\n"[0]
    end
  end

  def Template.process(input, replace)
    buf = ''
    input_mod = input[0..-1]
    parse_main(input_mod, buf, replace)
    return buf
  end
end

module Temp
  def Temp.temp_name(dir, n)
    ret = File.join(dir, 'eqe-' + n.to_s)
    return ret
  end

  def Temp.randomise(file)
    if file =~ /(.*?)(-\d+)?(\.[^\.]+)$/
      return $1 + '-' + rand(100000).to_s + $3
    else
      return file + '-' + rand(100000).to_s
    end
  end

  def Temp.make_temp_dir()
    # this should register the returned dir as deletable
    # and collect deletables on shutdown
    for try in (1..20)
      candidate = temp_name(Dir::tmpdir, rand(100000))
      begin
        FileUtils.mkdir candidate, :mode => 0700
        return candidate
      rescue => e
        log_exception(e)
      end
    end
    raise "Cannot create temporary directory: all attempts failed"
  end

  def Temp.with_temp_dir(&f)
    temp_dir = make_temp_dir()
    begin
      yield(temp_dir)
    ensure
      FileUtils.remove_dir temp_dir
    end
  end

  def Temp.install(src_dir, file, dest_dir)
    # puts "install file: " + File.join(src_dir, file)
    to_file(File.new(File.join(src_dir, file)).read, dest_dir, file)
  end

  def Temp.to_file(src, dest_dir, suggestion)
    #puts "installing " + file + " from " + src_dir + " to " + dest_dir
    begin
      size = src.size
    rescue => e
      log_exception(e)
      size = src.count
    end
    for try in (1..20)
      candidate = File.join(dest_dir, randomise(suggestion))
      begin
        File.open(candidate, File::WRONLY|File::EXCL|File::CREAT, 0666) do |dest|
          for i in 0...size
            dest.putc(src[i])
          end
        end
        return candidate
      rescue => e
        log_exception(e)
      end
    end
    raise "Cannot create temporary file in directory #{dest_dir}: all attempts failed"
  end
end

class TableInputStream
  def initialize(tab)
    @tab = tab
    @index = 0
  end
  def get!()
    if @index > @tab.size
      raise EOFError
    end
    ret = @tab[@index]
    @index += 1
    return ret
  end
  def remove!(elements)
    for e in elements
      get!() == e or raise "Remove: failure"
    end
  end
  def get_integer!(num_bytes)
    ret = 0
    for i in 1..num_bytes
      ret = (ret << 8) + get!()
    end
    return ret
  end
  def get_string!(num_chars)
    ret = ''
    for i in 1..num_chars
      ret << get!()
    end
    return ret
  end
end

class CrcComputer
  def initialize()
    @crc_table = make_crc_table()
  end
  def make_crc_table()
    crc_table = []
    for n in 0..255
      c = n
      for k in 0..7
        if (c & 1) != 0
          c = 0xedb88320 ^ (c >> 1)
        else
          c = c >> 1
        end
      end
      crc_table[n] = c
    end
    return crc_table
  end
  def update(crc, buf)
    buf.each_byte do |c|
      crc = @crc_table[(crc ^ c) & 0xff] ^ (crc >> 8)
    end
    return crc
  end
end

Crc = CrcComputer.new

def crc(*ss)
  ret = 0xffffffff
  for s in ss
    ret = Crc.update(ret, s)
  end
  ret = ret ^ 0xffffffff
  return ret
end

def stream_of_file(file)
  return TableInputStream.new(File.new(file).read())
end

class OutputStream
  def initialize(stream)
    @stream = stream
  end
  def put(byte)
    @stream.putc(byte)
  end
  def put_integer(n)
    for i in 0..3
      mask = 255 << ((3 - i) * 8)
      byte = (n & mask) >> ((3 - i) * 8)
      put(byte)
    end
  end
  def put_string(s)
    @stream.write(s)
  end
end

class StringOutputStream < OutputStream
  def initialize(string)
    @string = string
  end
  def put(byte)
    @string.concat(' ')
    @string[-1] = byte
  end
  def put_string(s)
    @string.concat(s)
  end
end

class Png
  Signature = [137, 80, 78, 71, 13, 10, 26, 10]

  def Png.parse_chunk(is)
    ret = {}
    begin
      ret['length'] = is.get_integer!(4)
    rescue => e
      # an exception is expected when EOF is reached
      # log_exception(e)
      return nil
    end
    ret['type'] = is.get_string!(4)
    ret['data'] = is.get_string!(ret['length'])
    ret['crc'] = is.get_integer!(4)
    return ret
  end

  def Png.make_chunk(type, data)
    chunk = {}
    chunk['length'] = data.size
    chunk['type'] = type
    chunk['data'] = data
    chunk['crc'] = crc(type, data)
    return chunk
  end

  def Png.change_chunk(chunks, type, data)
    new_chunk = make_chunk(type, data)
    already_added = false
    ret_chunks = []
    for chunk in chunks
      chunk_type = chunk['type']
      if chunk_type == type
        unless already_added
          ret_chunks.push(new_chunk)
          already_added = true
        end
      elsif chunk_type == 'IEND'
        unless already_added
          ret_chunks.push(new_chunk)
          already_added = true
        end
        ret_chunks.push(chunk)
      else
        ret_chunks.push(chunk)
      end
    end
    return ret_chunks
  end

  def Png.get_chunk(chunks, type)
    chunks.each do |chunk|
      if chunk['type'] == type
        return chunk['data']
      end
    end
    return nil
  end


  def Png.parse(is)
    is.remove!(Signature)
    chunks = []
    while chunk = parse_chunk(is)
      chunks.push(chunk)
    end
    return chunks
  end

  def Png.write_chunk(chunk, os)
    os.put_integer(chunk['length'])
    os.put_string(chunk['type'])
    os.put_string(chunk['data'])
    os.put_integer(chunk['crc'])
  end

  def Png.write(chunks, os)
    for i in Signature
      os.put(i)
    end
    for chunk in chunks
      write_chunk(chunk, os)
    end
  end

  Eqe_chunk_type = 'eqEx'

  def Png.with_metadata(s, data)
    chunks = parse(TableInputStream.new(s))
    chunks = change_chunk(chunks, Eqe_chunk_type, data)
    ret = ''
    write(chunks, StringOutputStream(ret))
    return ret
  end

  def Png.set_metadata(file, data)
    chunks = parse(TableInputStream.new(File.new(file).read))
    chunks = change_chunk(chunks, Eqe_chunk_type, data)
    f = File.new(file, 'w')
    write(chunks, OutputStream.new(f))
    f.close()
  end

  def Png.get_metadata_data(data)
    chunks = parse(TableInputStream.new(data))
    return get_chunk(chunks, Eqe_chunk_type)
  end

  def Png.get_metadata(file)
    return get_metadata_data(File.new(file).read)
  end
end

class Logger < Qt::Widget
  slots 'readStderr()', 'readStdout()'

  def initialize(process)
    super()
    @process = process
    Qt::Object.connect(process, SIGNAL('readyReadStandardError()'),
                       self, SLOT('readStderr()'))
    Qt::Object.connect(process, SIGNAL('readyReadStandardOutput()'),
                       self, SLOT('readStdout()'))
  end

  def readStderr()
    print "(WW) ",  @process.readAllStandardError
  end

  def readStdout()
    print '(II) ', @process.readAllStandardOutput
  end
end

class IoMemo < Qt::Widget
  slots 'readStderr()', 'readStdout()'
  attr_reader :stderr, :stdout

  def initialize(process)
    super()
    @process = process
    @stdout = Qt::ByteArray.new
    @stderr = Qt::ByteArray.new
    Qt::Object.connect(process, SIGNAL('readyReadStandardError()'),
                       self, SLOT('readStderr()'))
    Qt::Object.connect(process, SIGNAL('readyReadStandardOutput()'),
                       self, SLOT('readStdout()'))
  end

  def readStderr()
    @stderr.append(@process.readAllStandardError)
  end

  def readStdout()
    @stdout.append(@process.readAllStandardOutput)
  end
end

class Processus < Qt::Widget
  slots 'finished()'

  def initialize(program, args, callback_finished)
    super()
    @callback_finished = callback_finished
    @proc = Qt::Process.new
    @memo = IoMemo.new @proc
    Qt::Object.connect(@proc, SIGNAL('finished(int, QProcess::ExitStatus)'),
                       self, SLOT('finished()'))
    @proc.start(program, args)
    #proc.waitForFinished
    #ret = [memo.stdout, memo.stderr]
    #sleep 1
    # puts "run_process returns: #{ret[0].count} #{ret.join(',')}"
    #return ret
  end

  def finished()
    @callback_finished.call(@memo.stdout, @memo.stderr, @proc.exitCode)
  end
end

def run_synchronous_process(program, args, log, noraise = false)
  stdout = ''
  stderr = ''
  exit_code = 0
  done = false
  if log
    log.add_title "Running #{program} " + (args.map { |x| x.gsub(/ /, "\\'") }.join(' '))
  end
  Processus.new(program, args, lambda do |out, err, ec| stdout = out; stderr = err; exit_code = ec; done = true end)
  while not done
    Qt::CoreApplication::processEvents Qt::EventLoop::WaitForMoreEvents
  end
  if log
    log.add stdout
    log.add stderr, 'red'
    log.add "Exit code: #{exit_code}\n"
  end
  if exit_code != 0 and not noraise
    Kernel::raise "error: " + program + ' ' + args.map {|a| "'#{a}'"}.join(' ')
  end
  return [stdout, stderr, exit_code]
end

def edit_file(file, log, noraise = false)
  editors_to_try = ['/usr/bin/xdg-open', '/usr/bin/sensible-editor', '/usr/bin/gedit', '/usr/bin/kate']
  exception = nil
  editors_to_try.each do |editor|
    begin
      return run_synchronous_process(editor, [file], log)
    rescue => e
      log_exception(e)
    end
  end
  Kernel::raise "Cannot find editor to open file #{file}."
end

def qt_slurp(file)
  f = Qt::File.new(file)
  f.open(Qt::IODevice::ReadOnly)
  return f.readAll
end

def mime_data_copy(file, type)
  ret = Qt::MimeData.new
  ret.setData(type, qt_slurp(file))
  return ret
end

def mime_data_link(file)
  url = Qt::Url.new("file://" + file)
  ret = Qt::MimeData.new
  ret.setUrls([url])
  return ret
end

def change_extension(file, new_ext)
  return file.sub(/\.[^.]+$/, new_ext)
end

module Programs
  def Programs.latex_file(result_directory, options, log)
    latex = Template.process(options['template'], options)
    log.add_title "Expanding LaTeX template #{EqePath::LatexTemplate}"
    log.add latex
    return Temp.to_file(latex, result_directory, 'eqe-latex.tex')
  end

  def Programs.latex(latex_file, result_directory, options, log, latex_bin='latex', extension = '.dvi')
    ret = nil
    Temp.with_temp_dir {|dir|
      out = run_synchronous_process(latex_bin, ['-interaction=nonstopmode',
                                                '-output-directory', dir,
                                                latex_file], log)
      pdf_file = change_extension(File.basename(latex_file), extension)
      ret = Temp.install(dir, pdf_file, result_directory)
    }
    return ret
  end

  def Programs.dvips(dvi_file, result_dir, options, log)
    out = run_synchronous_process('/usr/bin/dvips', ['-E',
                                                     '-Ppdf',
                                                     '-o', '-', dvi_file], log)
    eps_file = change_extension(File.basename(dvi_file), '.eps')
    return Temp.to_file(out[0], result_dir, eps_file)
  end

  def Programs.epstopdf(eps_file, result_dir, options, log)
    out = run_synchronous_process('/usr/bin/epstopdf', ['--outfile', '-', eps_file], log)
    pdf_file = change_extension(File.basename(eps_file), '.pdf')
    return Temp.to_file(out[0], result_dir, pdf_file)
  end

  def Programs.epstosvg(eps_file, result_dir, options, log)
    out = run_synchronous_process('/usr/bin/pstoedit',
                                  ['-dt',
                                   '-ssp',
                                   '-usebbfrominput',
                                   '-f', 'plot-svg',
                                   eps_file
                                  ], log)
    svg = out[0].data
    svg = Svg.fix(svg, Eps.bounding_box(File.new(eps_file).read), options)
    svg_file = change_extension(File.basename(eps_file), '.svg')
    return Temp.to_file(svg, result_dir, svg_file)
  end

  def Programs.dvipng(dvi_file, result_dir, options, log)
    size = options['size'] || 30
    dpi = (size * 72) / 10
    ret = ''
    Temp.with_temp_dir do |dir|
      png_file = change_extension(File.basename(dvi_file), '.png')
      args = ['--png',
              '-T', 'tight',
              '-D', dpi.to_s,
              '-o', File.join(dir, png_file)]
      if options['background_color'] == nil
        args += ['-bg', 'Transparent']
      end
      out = run_synchronous_process('/usr/bin/dvipng', args + [dvi_file], log)
      Png.set_metadata(File.join(dir, png_file), YAML.dump(options))
      ret = Temp.install(dir, png_file, result_dir)
    end
    return ret
  end

  def Programs.convert(src_file, result_dir, options, log)
    format = nilor(options['output-format'], 'jpg')
    out = run_synchronous_process('/usr/bin/convert', ['-resize', '400x400', src_file, format + ':-'], log)
    #puts "convert: " + out[1].to_s
    dest_file = change_extension(File.basename(src_file), '.' + format)
    return Temp.to_file(out[0], result_dir, dest_file)
  end


  Indent = '&nbsp;&nbsp;'
  def Programs.htmlise(byte_array)
    return byte_array.constData.sub(/\s+$/, '').sub(/^/, Indent).gsub(/\n+/, "<br/>\n#{Indent}")
  end

  def Programs.debug_info
    test_me = [
               ['/bin/uname', '-srvmpio'],
               ['/usr/bin/lsb_release', '-a'],
               ['/bin/cat', '/etc/debian_version'],

               ['/usr/bin/ruby', '--version'],
               ['/usr/bin/apt-cache', 'showpkg', 'libqt4-ruby'],

               ['/usr/bin/pstoedit', '-v'],
               ['/usr/bin/dvipng', '--version'],
               ['/usr/bin/dvips', '--version'],
               ['/usr/bin/latex', '--version'],

               ['/usr/bin/xdg-open', '--version'],
              ]
    ret = "This is #{$NAME} version #{$VERSION}.<br/><br/>\n"
    for i in test_me
      prog = i[0]
      args = i[1..-1]
      line = [prog, *args].join(' ')
      ret += "<i>#{line}</i> "
      if File.exists? prog
        begin
          out = run_synchronous_process(prog, args, nil, true)
          if out[2] != 0
            ret += out[2].to_s + "<br/>\n"
          else 
            ret += "<br/>\n"
          end
          if out[0].size > 0
            ret += htmlise(out[0]) + "<br/>\n"
          end
          if out[1].size > 0
            ret += htmlise(out[1]) + "<br/>\n"
          end
        rescue => e
          ret += "error: #{e}<br/>\n"
        end
      else
        ret += "not found<br/>\n"
      end
    end
    return ret
  end
end

module Svg
  # Fixes the SVG files produced by epstosvg.
  # They have background and bounding box problems.
  def Svg.fix(svg, bounding_box, options)
    # remove the artificial white background
    svg.sub!(/<rect id="background"[^\/]*\/>/, '')
    # fix canvas size
    svg.sub! /<svg ([^>]+)>/ do
      "<svg #{fix_size($1, bounding_box, options)}>"
    end
    # fix transform
    svg.sub! /<g (id=\"content\" [^>]+)>/ do
      "<g #{fix_transform($1, bounding_box)}>"
    end
    return svg
  end

  def Svg.fix_size(attrs, bounding_box, options)
    w = bounding_box[1][0] - bounding_box[0][0]
    h = bounding_box[1][1] - bounding_box[0][1]
    w_in = 4
    h_in = w_in * h / w
    attrs.sub!(/width="[^"]+"/, "width=\"#{w_in}in\"")
    attrs.sub!(/height="[^"]+"/, "height=\"#{h_in}in\"")
    attrs.sub!(/viewBox="[^"]+"/, "viewBox=\"0 0 #{w} #{h}\"")
    return attrs
  end

  def Svg.fix_transform(attrs, bounding_box)
    dx = -bounding_box[0][0]
    dy = -bounding_box[1][1]
    attrs.sub!(/transform="[^"]+"/,
               "transform=\"scale(1, -1) translate(#{dx}, #{dy})\"")
    return attrs
  end
end

module Eps
  def Eps.bounding_box(eps)
    if eps !~ /%%BoundingBox: ([\d.]+) ([\d.]+) ([\d.]+) ([\d.]+)/
      raise 'Cannot read EPS bounding_box'
    end
    return [[$1.to_f, $2.to_f], [$3.to_f, $4.to_f]]
  end
end

class Latex_formulator
  def initialize(working_dir)
    @working_dir = working_dir

    @formatters = {
      'latex' => [:latex_file],
      'pdf' => [:epstopdf, 'eps'],
      'dvi' => [:latex, 'latex'],
      'eps' => [:dvips, 'dvi'],
      'png' => [:dvipng, 'dvi'],
      'svg' => [:epstosvg, 'eps']
    }
    @cache = {}
    @input_latex = '(no input)'
  end

  def reset_cache()
    @cache.each do |format, file| File.unlink file end
    @cache = {}
  end

  def set_input(input_latex)
    unless input_latex =~ /^\s*$/
      @input_latex = input_latex
      reset_cache()
    end
  end

  def formats()
    return @formatters
  end

  def file(format, options, log)
    if not @cache[format]
      if not formatter = @formatters[format]
        raise "LaTeX formula maker: unsupported format: " + format
      end
      if src = formatter[1]
        @cache[format] = Programs.send(formatter[0], file(src, options, log), @working_dir, options, log)
      else
        @cache[format] = Programs.send(formatter[0], @working_dir, options, log)
      end
    end
    return @cache[format]
  end

  def file_async(format, options, log, &callback_finished)
    log.clear
    ret = file(format, options, log)
    callback_finished.call(ret)
  end

  def set_log(log)
    @log = log
  end
end

class Updater < Qt::Widget
  slots 'update()', 'dirtied()', 'updateFinished(QString)'
  
  def initialize(ui)
    @ui = ui
    super() # @ui.main_window) # passing the parent hides the first menus from clicks
    @dirty = false
  end

  def init_connections()
    Qt::Object.connect(@ui.action_update, SIGNAL('triggered()'),
                       self, SLOT('update()'))
    Qt::Object.connect(@ui.input_zone, SIGNAL('textChanged()'),
                       self, SLOT('dirtied()'))
    Qt::Object.connect(@ui.option_pane, SIGNAL('dirty()'),
                       self, SLOT('dirtied()'))
    update
  end

  def updateFinished(png_file)
    @ui.image_zone.set_image(png_file)
    end_update()
  end

  def start_update()
    # puts "starting update..."
    @updating = true
    @dirty = false
    @ui.progress_bar.show
    @ui.signal_status "Generating image..."
    @ui.enable(false)
  end

  def end_update()
    @ui.clear_status
    @ui.progress_bar.hide
    @ui.enable()
    @updating = false
    @ui.option_pane.save
    if @dirty
      dirtied
    end
  end

  def update()
    start_update()
    @ui.generator.set_input @ui.input_zone.plainText
    @ui.generator.reset_cache
    begin
      @ui.generator.file_async('png', @ui.option_pane.options, @ui.log_window) do |f|
        updateFinished(f)
      end
    rescue => e
      log_exception(e)
      @ui.progress_bar.hide
      @ui.enable
      @updating = false
      @ui.log_window.add e, 'red'
      @ui.signal_error 'Error generating the PNG file.'
    end
  end

  def generate(format, options, &callback_finished)
    start_update()
    begin
      @ui.generator.file_async(format, options, @ui.log_window) do |f|
        end_update();
        callback_finished.call(f)
      end
    rescue => e
      log_exception(e)
      @ui.progress_bar.hide
      @ui.enable
      @updating = false
      @ui.log_window.add e, 'red'
      @ui.signal_error 'Error generating #{format.upcase} file.'
    end
  end

  def dirtied()
    # puts "marking dirty"
    @ui.signal_status "Image is not up to date."
    @dirty = true
    if @ui.option_pane.auto_update and not @updating
      update
    end
  end
end

class Config < Qt::Object
  slots 'save()'

  def initialize(name, parent)
    super(parent)
    @file = File.join(EqePath::ConfigDirectory, 'eqe-' + name + '.yaml')
    begin
      @config = YAML.load_file @file
      @config['OK'] = 1
    rescue
      @config = {}
    end
    @properties = {}
  end

  def add_property(p)
    p[:name].nil? and raise "Config.add_property: nil name"
    p[:obj].nil? and raise "Config.add_property: nil object"
    p[:prop].nil? and raise "Config.add_property: nil property"
    val = nilor(@config[p[:name]], p[:default])
    unless val.nil?
      begin
        p[:obj].send(p[:prop].to_s + '=', val)
      rescue => e
        log_exception e
        begin
          unless p[:default].nil?
            p[:obj].send(p[:prop].to_s + '=', p[:default])
          end
        rescue => e
          log_exception e
        end
      end
    end
    @properties[p[:name]] = p
    # It is better to save only when generating the image has succeeded.
    #     if p[:signal]
    #       Qt::Object.connect(p[:obj], SIGNAL(p[:signal]),
    #                          self, SLOT('save()'))
    #     end
  end

  def default(prop)
    return @properties[prop][:default]
  end

  def add_checkbox(name, obj, default_value = Qt::Checked, *args)
    add_property({:name => name,
                   :obj => obj,
                   :prop => :checkState,
                   :default => default_value}.merge(Hash[*args]))
                 # :signal => 'stateChanged(int)')
  end

  def update(tag=nil)
    @properties.each do |name, p|
      if (not tag) or p[:tag] == tag
        begin
          @config[name] = p[:obj].send(p[:prop])
        rescue => e
          log_exception e
          raise
        end
      end
    end
  end

  def save(tag=nil)
    begin
      update(tag)
      File.open(@file, 'w') do |out|
        YAML.dump @config, out
      end
    rescue => e
      log_exception e
      parent.log_window.add e, 'red'
      parent.signal_status "Error: could not save interface state."
    end
  end
end

# to be mixed in
module Draggable
  attr_writer :disable_drag

  def mousePressEvent(event)
    super(event)
    if event.button == Qt::LeftButton
      @drag_start_position = event.pos
    end
  end

  def mouseMoveEvent(event)
    super(event)
    if @disable_drag
      return
    end
    if (event.buttons.to_i & Qt::LeftButton.to_i) == 0
      return
    end
    return unless @drag_start_position
    # warning, this segfaults if @dragStartPosition is undefined
    delta = event.pos - @drag_start_position
    if delta.manhattanLength < Qt::Application::startDragDistance
      return
    end
    do_dnd()
    # it is possible to have two modes : with automatic retry, and without
    #do_dnd
  end

end

class DragButton < Qt::ToolButton
  include Draggable
  slots 'save_as()', 'open()'

  def initialize(parent, ui, format, label, as_data = false)
    super(parent)
    setText(label)
    Qt::Object.connect(self, SIGNAL('clicked()'),
                       self, SLOT('open()'))
    @ui = ui
    @as_data = as_data
    @format = format
    @action_save = Qt::Action.new("Save as...", self)
    set_icon @action_save, 'save-as'
    @action_open = Qt::Action.new("Open", self)
    set_icon @action_open, 'open'
    @menu = Qt::Menu.new self
    @menu.addAction @action_open
    @menu.addAction @action_save
    Qt::Object.connect(@action_save, SIGNAL('triggered()'),
                       self, SLOT('save_as()'))
    Qt::Object.connect(@action_open, SIGNAL('triggered()'),
                       self, SLOT('open()'))
    setMenu @menu
    setPopupMode(Qt::ToolButton::MenuButtonPopup)
    tooltip = "Click to open, unfold to save, drag to send to other application."
    tooltip += "<br />The equation is in #{format} format, and sent as "
    if as_data
      tooltip += "data to include directly."
    else
      tooltip += "a file to link to."
    end
    setToolTip tooltip
  end

  def do_dnd()
    @ui.dnd(@format, @as_data)
  end

  def save_as()
    @ui.save_as(@format)
  end

  def open()
    @ui.open(@format)
  end
end

class ResizeHandle < Qt::GraphicsRectItem
  def initialize(bounding_rect, image_zone)
    super()#parent)
    @image_zone = image_zone
    @bounding_rect = bounding_rect
    r = Qt::RectF.new
    @side = 12
    r.top = bounding_rect.top + bounding_rect.height - @side / 2
    r.left = bounding_rect.left + bounding_rect.width - @side / 2
    r.width = @side
    r.height = @side
    setRect r
    black_brush = Qt::Brush.new Qt::SolidPattern
    setBrush black_brush
    setPen Qt::Pen.new
    @x = 0
    setCursor(Qt::Cursor.new(Qt::SizeFDiagCursor))
  end

  def mousePressEvent(event)
    @image_zone.disable_drag = true
  end

  def mouseReleaseEvent(event)
    @image_zone.propagate_scale()
    @image_zone.disable_drag = false
  end

  def mouseMoveEvent(event)
    x = [5, event.pos.x - @bounding_rect.left].max
    y = [5, event.pos.y - @bounding_rect.top].max
    w = @bounding_rect.width
    h = @bounding_rect.height
    k = (w * x + h * y) / (w*w + h*h)
    r = self.rect
    #puts "#{event.pos.x} #{event.pos.y}"
    r.left = k * w + @bounding_rect.left  - @side / 2
    r.top = k * h + @bounding_rect.top - @side / 2
    r.width = @side
    r.height = @side
    setRect r
    @image_zone.set_scale(k)
  end
end

class ImageZone < Qt::GraphicsView
  include Draggable

  attr :generator

  def initialize(parent, ui)
    super(parent)
    @ui = ui
    gradient = Qt::LinearGradient.new(-200.0, -200.0, 150.0, 150.0)
    #self.backgroundBrush = Qt::Brush.new gradient
    self.cursor = Qt::Cursor.new Qt::OpenHandCursor
    @scale = 1
    setStyleSheet('background:transparent')
    setFrameStyle 0
    setFocusPolicy(Qt::NoFocus)
    setToolTip "Dragging sends the equation as a PNG file.<br />Use the black square in the lower right corner to resize."
    setAcceptDrops true
  end

  def file_in_mime(mime)
    d = mime.data('text/uri-list').constData.chomp
    return URI.unescape(URI.parse(d).path)
  end

  def dragEnterEvent(e)
    md = e.mimeData
    if md.hasFormat 'image/png'
      e.acceptProposedAction
    end
    if md.hasFormat 'text/uri-list'
      file = file_in_mime(md)
      if File.exists? file
        e.acceptProposedAction
      else
        puts "file does not seem to exist: #{file}"
      end
    end
  end

  def dragMoveEvent(e)
  end

  def dragLeaveEvent(e)
  end

  def dropEvent(e)
    md = e.mimeData
    if md.hasFormat 'image/png'
      @ui.option_pane.load_png_data(e.mimeData.constData('image/png'))
      return
    end
    if md.hasFormat 'text/uri-list'
      @ui.option_pane.load_png(file_in_mime(md))
      return
    end
  end

  def set_generator(generator)
    @generator = generator
  end

  def do_dnd()
    @ui.dnd('png', false)
  end

  def set_image(png_file)
    pixmap = Qt::Pixmap.new png_file
    @pixmap_item = Qt::GraphicsPixmapItem.new(pixmap)
    @scene = Qt::GraphicsScene.new
    @scene.addItem @pixmap_item
    bounding_rect = @pixmap_item.boundingRect
    pen = Qt::Pen.new
    pen.setStyle Qt::DashLine
    @rect_around = @scene.addRect(bounding_rect, pen)
    @resize_handle = ResizeHandle.new(bounding_rect, self)
    @scene.addItem @resize_handle
    setScene @scene
  end

  def set_scale(s)
    @scale = s
    @pixmap_item.setTransform(Qt::Transform.new.scale(s, s))
    @rect_around.setRect(@pixmap_item.sceneBoundingRect)
  end

  def image_height()
    if @pixmap_item
      h = @pixmap_item.boundingRect.height
      # puts @scene.sceneRect.height
      # puts self.rect.height
      # puts "raw h: " + h.to_s
      h = @pixmap_item.mapToScene(0, h).y
      h = self.mapFromScene(0, h).y
      # puts "mapped h: " + h.to_s
      return h
    else
      return 0
    end
  end

  def propagate_scale()
    new_value = @ui.option_pane.size_spin.value * @scale
    if new_value != new_value # NaN
      new_value = 1
    end
    @ui.option_pane.size_spin.value = new_value
    @scale = 1
  end
end

class ColorPicker < Qt::ComboBox
  def initialize(parent, default_color, allow_transparent = false)
    super(parent)
    @builtin_colors = {
      'red' => [1.0, 0.0, 0.0],
      'green' => [0.0, 1.0, 0.0],
      'blue' => [0.0, 0.0, 1.0],
      'black' => [0.0, 0.0, 0.0],
      'white' => [1.0, 1.0, 1.0],
      'transparent' => 'Transparent'
    }
    @default_index = 0
    @index_of_value = {}
    @builtin_colors.keys.sort.each do |k|
      if allow_transparent or k != 'transparent'
        addItem k
        if k == default_color
          @default_index = count() - 1
        end
        @index_of_value[tex(k)] = count() - 1
        @index_of_value[k] = count() - 1
      end
    end
    self.currentIndex = @default_index
  end

  def tex(v = currentText())
    c = @builtin_colors[v]
    unless c
      raise "No such color: #{v}."
    end
    if c == 'Transparent'
      return nil
    else
      return c.join(',')
    end
  end

  def set(color)
    index = @index_of_value[color]
    unless index
      raise "No such color: #{color}."
    end
    if index >= 0
      self.setCurrentIndex(index)
    else
      # self.parent.ui.status_bar.showMessage "Could not load color named #{color}"
      self.setCurrentIndex(@default_index)
    end
  end
  def color()
    return tex()
  end
  def color=(c)
    set(c)
  end
end

class FontPicker < Qt::ComboBox
  def initialize(parent, default_font)
    super(parent)
    # http://web.image.ufl.edu/help/latex/fonts.shtml
    @builtin_fonts = {
      'Times (narrow Courier)' => 'pslatex',
      'Times' => 'times',
      'New Century' => 'newcent',
      'Palatino' => 'palatino',
      # 'Palatino (CM math)' => 'palatcm',
      'Bookman' => 'bookman',
      'Utopia' => 'utopia',
      # 'Lmodern' => 'lmodern',
      'Computer Modern Bright' => 'cmbright',
      'Concrete, Euler math' => 'ccfonts,eulervm',
      'Concrete' => 'ccfonts',
      'Iwona' => 'iwona',
      'Helvetica' => 'helvet',
      'Palatino, pxfonts math' => 'pxfonts',
      'Palatino, mathpazo math' => 'mathpazo',
      'Palatino, Euler math' => 'mathpple',
      'Arev' => 'arev',
      'Utopia, Fourier-GUTenberg math' => 'fourier'
    }
    @default_index = 0
    @index_of_value = {}
    @builtin_fonts.keys.sort.each do |k|
      addItem k
      if @builtin_fonts[k] == default_font
        @default_index = count() - 1
      end
      @index_of_value[@builtin_fonts[k]] = count() - 1
      @index_of_value[k] = count() - 1
    end
    self.currentIndex = @default_index
  end

  def tex_font()
    ret = @builtin_fonts[currentText()]
    unless ret
      raise "No such font: #{currentText()}."
    end
    return ret
  end

  def set(font)
    index = @index_of_value[font]
    unless index
      raise "No font selected."
    end
    if index >= 0
      self.setCurrentIndex(index)
    else
      # self.parent.ui.status_bar.showMessage "Could not load font named #{font}"
      self.setCurrentIndex(@default_index)
    end
  end
  def font=(f)
    set(f)
  end
  def font()
    return tex_font()
  end
end

class OptionPane < Qt::Widget
  slots 'toggle_show()', 'auto_update_toggled()', 'save()',
  'bigger()', 'smaller()', 'open_template()', 'reset_options()'
  signals 'dirty()'

  attr_reader :size_spin, :ui

  def auto_update()
    return (@auto_update_checkbox.checkState == Qt::Checked)
  end

  def auto_update_toggled()
    if auto_update
      @ui.updater.dirtied
      @ui.action_update.visible = false
    else
      @ui.action_update.visible = true
    end
  end

  def add_line(vertical_layout)
    line = Qt::Widget.new self
    line_layout = Qt::HBoxLayout.new line
    line_layout.setAlignment Qt::AlignLeft
    line_layout.setContentsMargins 0, 0, 0, 0
    vertical_layout.addWidget line
    return line_layout
  end

  def add(line, widget, text = nil)
    if text
      label = Qt::Label.new(text, @option_group)
      label.setBuddy widget
      line.addWidget(label, 0, Qt::AlignRight)
    end
    line.addWidget(widget, 0, Qt::AlignLeft)
  end

  def initialize(parent, configuration, ui)
    super(parent)
    @configuration = configuration

    @ui = ui
    @layout = Qt::VBoxLayout.new self
    @layout.setContentsMargins 0, 0, 0, 0

    @option_group = Qt::Widget.new self
    @options_layout = Qt::VBoxLayout.new @option_group

    line = add_line(@options_layout)

    @auto_update_checkbox = Qt::CheckBox.new 'Auto-&update', self
    @auto_update_checkbox.setToolTip "Updates the display automatically when input or an option is changed."
    @configuration.add_checkbox('auto-update',
                                @auto_update_checkbox,
                                Qt::Checked,
                                :tag => :cosmetic)
    add(line, @auto_update_checkbox)

    @displaymath_checkbox = Qt::CheckBox.new 'Wrap in &displaymath', self
    @displaymath_checkbox.setToolTip "Wraps your LaTeX input in a displaymath environment."
    @configuration.add_checkbox('displaymath',
                                @displaymath_checkbox)
    add(line, @displaymath_checkbox)

    @open_template_action = Qt::Action.new "&Edit LaTeX template...", self
    set_icon @open_template_action, 'properties'
    
    @open_template_button = Qt::ToolButton.new self
    @open_template_button.toolButtonStyle = Qt::ToolButtonTextBesideIcon
    @open_template_button.setDefaultAction @open_template_action

    add line, @open_template_button

    line = add_line(@options_layout)

    @font_picker = FontPicker.new(self, 'pslatex')
    @configuration.add_property(:name => 'font',
                                :obj => @font_picker,
                                :prop => :font,
                                :default => 'pslatex')
                                #:signal => 'currentIndexChanged(int)')
    add(line, @font_picker, "Fon&t:")

    @size_spin = Qt::SpinBox.new @option_group
    @size_spin.minimum = 2
    @size_spin.maximum = 1000
    @configuration.add_property(:name => 'size',
                                :obj => @size_spin,
                                :prop => :value,
                                :default => 47)
                                #:signal => 'valueChanged(int)')
    add(line, @size_spin, "&Size:")

    line = add_line(@options_layout)

    @foreground_color_picker = ColorPicker.new(self, 'black')
    @configuration.add_property(:name => 'foreground-color',
                                :obj => @foreground_color_picker,
                                :prop => :color,
                                :default => 'black')
                                #:signal => 'currentIndexChanged(int)')
    add(line, @foreground_color_picker, "&Foreground:")

    @background_color_picker = ColorPicker.new(self, 'transparent', true)
    @configuration.add_property(:name => 'background-color',
                                :obj => @background_color_picker,
                                :prop => :color,
                                :default => 'transparent')
                                #:signal => 'currentIndexChanged(int)')
    add(line, @background_color_picker, "&Background:")
    
    @reset_options_action = Qt::Action.new "&Reset defaults", self
    set_icon @reset_options_action, 'undo-ltr'

    @reset_options_button = Qt::ToolButton.new self
    @reset_options_button.toolButtonStyle = Qt::ToolButtonTextBesideIcon
    @reset_options_button.setDefaultAction @reset_options_action
    
    add line, @reset_options_button

    @layout.addWidget @option_group

    @show_button = Qt::ToolButton.new @central_widget
    @show_button.toolButtonStyle = Qt::ToolButtonTextBesideIcon
    @layout.addWidget @show_button
    @configuration.add_property(:name => 'show-options',
                                :obj => self,
                                :prop => :show_options,
                                :default => false,
                                :tag => :cosmetic)
    show_options = true
    Qt::Object.connect(@show_button, SIGNAL('clicked()'),
                       self, SLOT('toggle_show()'))

    Qt::Object.connect(@open_template_action, SIGNAL('triggered()'),
                       self, SLOT('open_template()'))
    Qt::Object.connect(@reset_options_action, SIGNAL('triggered()'),
                       self, SLOT('reset_options()'))

    # things that make the equation not up-to-date
    Qt::Object.connect(@size_spin, SIGNAL('valueChanged(int)'),
                       self, SIGNAL('dirty()'))
    Qt::Object.connect(@foreground_color_picker, SIGNAL('currentIndexChanged(int)'),
                       self, SIGNAL('dirty()'))
    Qt::Object.connect(@background_color_picker, SIGNAL('currentIndexChanged(int)'),
                       self, SIGNAL('dirty()'))
    Qt::Object.connect(@font_picker, SIGNAL('currentIndexChanged(int)'),
                       self, SIGNAL('dirty()'))
    Qt::Object.connect(@displaymath_checkbox, SIGNAL('stateChanged(int)'),
                       self, SIGNAL('dirty()'))

    Qt::Object.connect(@auto_update_checkbox, SIGNAL('stateChanged(int)'),
                       self, SLOT('auto_update_toggled()'))
  end

  def open_template
    begin
      edit_file(EqePath::LatexTemplate, @ui.log_window)
    rescue => e
      log_exception e
      if @ui.status_bar
        @ui.log_window.add e, 'red'
        @ui.signal_error "Error: could not open the template."
      end
    end
  end

  def save(tag=nil)
    begin
      @configuration.save(tag)
    rescue => e
      log_exception e
      if @ui.status_bar
        @ui.log_window.add e, 'red'
        @ui.signal_error "Error: could not save options."
      end
    end
  end

  def load_png(file)
    set_options(YAML.load(Png.get_metadata(file)))
  end

  def load_png_data(data)
    set_options(YAML.load(Png.get_metadata_data(data)))
  end

  def toggle_show()
    self.show_options = ! self.show_options
  end

  def show_options
    return @options_shown
    # @option_group.visible is not usable after the main window was closed
  end

  def show_options=(b)
    @options_shown = b
    if b
      @option_group.show
      @show_button.arrowType = Qt::UpArrow
      @show_button.text = 'Hide optio&ns'
      @show_button.setToolTip "Click to hide the options."
    else
      @option_group.hide
      @show_button.arrowType = Qt::RightArrow
      @show_button.text = 'Show optio&ns'
      @show_button.setToolTip "Click to show the options for generating the equation."
    end
    #save
  end

  DefaultLatexTemplate = '
%% This is a LaTeX template for eqe.
%% You may want to adjust the included packages at the beginning.

\documentclass{article}
\pagestyle{empty}

\usepackage[T1]{fontenc}
\usepackage[latin1]{inputenc}
%\usepackage[french]{babel}
\usepackage[dvips]{graphicx}
\usepackage{color}

\usepackage{amsmath}

[%- IF font -%]
\usepackage{[% font %]}
[%- END -%]

\begin{document}

[% IF foreground_color -%]
\color[rgb]{[% foreground_color %]}
[%- END %]

[% IF background_color -%]
\pagecolor[rgb]{[% background_color %]}
[%- END %]

[%- IF displaymath -%]
\begin{displaymath}
[%- END -%]
[% input %]
[%- IF displaymath -%]
\end{displaymath}
[%- END -%]

\end{document}
'

  def latex_template
    begin
      if not File.exists? EqePath::LatexTemplate
        FileUtils.mkdir_p([EqePath::ConfigDirectory])
        File.open(EqePath::LatexTemplate, 'w') do |f|
          f.write DefaultLatexTemplate
        end
      end
      return File.new(EqePath::LatexTemplate).read
    rescue => e
      log_exception e
      @ui.log_window.add e, 'red'
      @ui.signal_error "Error: could not initialize LaTeX template."
      return DefaultLatexTemplate
    end
  end

  def options
    return {
      'size' => @size_spin.value,
      'displaymath' => @displaymath_checkbox.checkState == Qt::Checked,
      'foreground_color' => @foreground_color_picker.tex,
      'background_color' => @background_color_picker.tex,
      'font' => @font_picker.tex_font,
      'input' => @ui.input_zone.plainText.gsub(/\s+$/, ''),
      'format' => 'LaTeX',
      'template' => latex_template
    }
  end

  def bigger
    @size_spin.value += 2
  end
  def smaller
    @size_spin.value -= 2
  end

  def set_options(opts)
    begin
      blockSignals(true)
      @ui.input_zone.blockSignals(true)
      @size_spin.value = nilor(opts['size'], @configuration.default('size'))
      @foreground_color_picker.set(nilor(opts['foreground_color'], @configuration.default('foreground-color')))
      @background_color_picker.set(nilor(opts['background_color'], @configuration.default('background-color')))
      @font_picker.set(nilor(opts['font'], @configuration.default('font')))
      @displaymath_checkbox.checkState = nilor(opts['displaymath'], @configuration.default('displaymath')) ? Qt::Checked : Qt::Unchecked
      @ui.input_zone.setText(nilor(opts['input'], @configuration.default('input')))
    ensure
      blockSignals(false)
      @ui.input_zone.blockSignals(false)
    end
    emit dirty()
  end

  def reset_options
    opts = options
    ['size', 'foreground_color', 'background_color', 'font'].each do |o| opts.delete o end
    set_options(opts)
  end
end

def set_icon(action, id)
  for theme in ['Human', 'gnome']
    file = "/usr/share/icons/#{theme}/16x16/actions/gtk-#{id}.png"
    if File.exists? file
      action.icon = Qt::Icon.new(file)
      return
    end
  end
end

def truncate_string(s, len)
  if s.size <= len
    return s
  else
    return s[0..len-3] + '...'
  end
end

class LogWindow < Qt::Dialog
  def initialize(parent = nil)
    super(parent)
    @contents = Qt::TextEdit.new @problem_dialog
    @contents.setReadOnly true
    layout = Qt::VBoxLayout.new self
    layout.addWidget @contents
    @contents.setAlignment Qt::AlignJustify
    resize 600, 400
    #enable
  end
  def clear
    @contents.clear
  end
  def add_title(t)
    @contents.insertHtml "<b>#{t}</b><br />\n"
  end
  def add(m, color=nil)
    if color
      @contents.setTextColor(Qt::Color.new(color))
      @contents.insertPlainText m.to_s
      @contents.setTextColor(Qt::Color.new('black'))
    else
      @contents.insertPlainText m.to_s
    end
  end
  def enable
    self.show
    self.raise
    self.activateWindow
  end
end

class Ui < Qt::Widget
  attr_reader :main_window

  attr_reader :input_zone
  attr_reader :image_zone
  attr_reader :generator
  attr_reader :status_bar
  attr_reader :progress_bar
  attr_reader :action_update
  attr_reader :option_pane
  attr_reader :updater
  attr_reader :log_window

  slots 'open_library()', 'open_equation()', 'about()', 'save_cosmetic()',
  'in_case_of_problem()', 'show_log()'

  def save(tag=nil)
    @option_pane.save tag
  end

  def save_cosmetic
    # puts "saving cosmetic options"
    save(:cosmetic)
  end

  def show_log
    @log_window.show
  end

  def initialize(working_dir)
    super()
    init_library()
    @working_dir = working_dir
    @main_window = Qt::MainWindow.new
    init_actions @main_window
    init_widgets @main_window
    @updater = Updater.new self
    @input_zone.setFocus
    # @input_zone.setText('\vec{\nabla} \times {\color{blue}\vec{B}} = \frac{\partial {\color{red}\vec{E}}} {\partial t} + 4 \pi \vec{J}_e')
    init_connections @main_window
    @updater.init_connections
  end

  def init_library()
    begin
      FileUtils.mkdir_p([EqePath::Library])
    rescue => e
      log_exception e
    end
  end

  def open_library()
    run_synchronous_process('/usr/bin/xdg-open', [EqePath::Library], @log_window)
  end

  def init_actions(main_window)
    # main menu actions
    @action_library = Qt::Action.new "&Open library", main_window
    set_icon @action_library, 'home'
    @action_open = Qt::Action.new "Open &equation...", main_window
    set_icon @action_open, 'open'
    @action_open.shortcut = Qt::KeySequence.new Qt::KeySequence.Open
    @action_quit = Qt::Action.new "&Quit", main_window
    set_icon @action_quit, 'quit'
    @action_quit.shortcut = Qt::KeySequence.new Qt::KeySequence.Close

    # LaTeX zone actions
    @action_undo = Qt::Action.new "&Undo", main_window
    @action_redo = Qt::Action.new "&Redo", main_window
    @action_clear = Qt::Action.new "&Clear", main_window

    # image zone actions
    @action_update = Qt::Action.new "&Update", main_window
    set_icon @action_update, 'refresh'
    @action_update.shortcut = Qt::KeySequence.new Qt::KeySequence.Refresh
    @action_update.visible = false
    # @action_cancel_update = Qt::Action.new "&Cancel update", main_window
    
    # Zoom
    @action_bigger = Qt::Action.new "&Bigger", main_window
    set_icon @action_bigger, 'zoom-in'
    @action_bigger.shortcut = Qt::KeySequence.new Qt::KeySequence.ZoomIn
    @action_smaller = Qt::Action.new "&Smaller", main_window
    set_icon @action_smaller, 'zoom-out'
    @action_smaller.shortcut = Qt::KeySequence.new Qt::KeySequence.ZoomOut

    # help actions
    @action_about = Qt::Action.new "&About #{$NAME}", main_window
    set_icon @action_about, 'about'

    @action_problem = Qt::Action.new "In case of problem", main_window
    set_icon @action_problem, 'go-forward-ltr'
  end

  def set_input_font(family)
    @input_font = Qt::Font.new
    @input_font.setFamily family
    @input_zone.setFont @input_font
  end

  def add_drag_buttons(toolbar, generator)
    interesting = [
      ['latex', ['LaTeX', false]],
      ['eps', ['EPS', false]],
      ['pdf', ['PDF', false]],
      ['svg', ['SVG data', true], ['SVG', false]],
      ['png', ['PNG data', true], ['PNG', false]],
    ]
    generator_formats = generator.formats
    for format in interesting
      if generator_formats[format[0]]
        for args in format[1..-1]
          button = DragButton.new(toolbar, self, format[0], *args)
          toolbar.addWidget button
        end
      end
    end
  end

  def set_generator(generator)
    @generator = generator
    @image_zone.set_generator(generator)
    @tool_bar.clear
    @tool_bar.toolButtonStyle = Qt::ToolButtonTextBesideIcon
    @tool_bar.addAction @action_update
    # @auto_update_checkbox = Qt::CheckBox.new("Auto update", @tool_bar)
    # @auto_update_checkbox.checkState = Qt::Checked
    # @tool_bar.addWidget @auto_update_checkbox
    generator.set_log @log_window
    add_drag_buttons(@tool_bar, @generator)
  end

  def position
    p = @main_window.pos
    return [p.x, p.y]
  end

  def position=(p)
    @main_window.move p[0], p[1]
  end

  def size
    s = @main_window.size
    return [s.width, s.height]
  end

  def size=(s)
    @main_window.resize s[0], s[1]
  end

  def init_widgets(main_window)
    @configuration = Config.new 'ui-state', self

    @configuration.add_property(:name => 'main-window-position',
                                :obj => self,
                                :prop => :position,
                                :tag => :cosmetic)

    @configuration.add_property(:name => 'main-window-size',
                                :obj => self,
                                :prop => :size,
                                :default => [580, 560],
                                :tag => :cosmetic)

    @central_widget = Qt::Widget.new main_window
    @central_layout = Qt::VBoxLayout.new @central_widget
    main_window.setCentralWidget @central_widget

    # vertical splitter, separating image zone and input zone
    @splitter = Qt::Splitter.new @central_widget
    @splitter.setOrientation(Qt::Vertical)
    @splitter.childrenCollapsible = false

    # image zone
    @image_zone = ImageZone.new(@splitter, self)

    # LaTeX input zone
    @input_zone = Qt::TextEdit.new @splitter
    set_input_font("DejaVu Sans Mono")
    @input_zone.setAcceptRichText false
    @input_zone.setToolTip "Type here your LaTeX input."
    @configuration.add_property(:name => 'input',
                                :obj => @input_zone,
                                :prop => :plainText,
                                :default => '\vec{\nabla} \times {\color{blue}\vec{B}} = \frac{\partial {\color{red}\vec{E}}} {\partial t} + 4 \pi \vec{J}_e')

    @central_layout.addWidget @splitter

    @configuration.add_property(:name => 'splitter-state',
                                :obj => @splitter,
                                :prop => :sizes,
                                :tag => :cosmetic)

    @option_pane = OptionPane.new(@central_widget, @configuration, self)
    @central_layout.addWidget @option_pane

    #     @size_slider = Qt::Slider.new Qt::Horizontal, self
    #     @central_layout.addWidget @size_slider

    @menu_bar = Qt::MenuBar.new main_window
    @menu_eqe = Qt::Menu.new @menu_bar
    # @menu_latex = Qt::Menu.new @menu_bar
    # @menu_image = Qt::Menu.new @menu_bar
    @menu_help = Qt::Menu.new @menu_bar

    @menu_eqe.setTitle "&#{$NAME}"
    # @menu_latex.setTitle "&LaTeX"
    # @menu_image.setTitle "&Image"
    @menu_help.setTitle "&Help"

    @menu_bar.addAction @menu_eqe.menuAction
    # @menu_bar.addAction @menu_latex.menuAction
    # @menu_bar.addAction @menu_image.menuAction
    @menu_bar.addAction @menu_help.menuAction
    
    @menu_eqe.addAction @action_library
    @menu_eqe.addAction @action_open
    @menu_eqe.addSeparator
    @menu_eqe.addAction @action_quit

    @menu_various = Qt::Menu.new @menu_bar
    @menu_bar.addAction @menu_various.menuAction
    @menu_various.addAction @action_bigger
    @menu_various.addAction @action_smaller
    @menu_various.visible = false

    # @menu_latex.addAction @action_undo
    #     @menu_latex.addAction @action_redo
    #     @menu_latex.addAction @action_clear
    #     @menu_image.addAction @action_update
    # @menu_image.addAction @action_cancel_update
    @menu_help.addAction @action_about
    @menu_help.addAction @action_problem
    main_window.setMenuBar @menu_bar

    @tool_bar = main_window.addToolBar("Actions")
    @tool_bar.toggleViewAction.visible = false
    @tool_bar.addAction @action_update

    @status_bar = Qt::StatusBar.new main_window
    main_window.setStatusBar @status_bar
    @progress_bar = Qt::ProgressBar.new @status_bar
    @progress_bar.minimum = @progress_bar.maximum = 0
    @status_bar.addPermanentWidget @progress_bar

    @status_show_log_button = Qt::PushButton.new 'Show &error log...', @status_bar
    @status_bar.addPermanentWidget @status_show_log_button
    @status_show_log_button.hide

    @log_window = LogWindow.new main_window

    set_generator(Latex_formulator.new(@working_dir))
  end

  def enable(en = true)
    if en
      verbose_message("enabling UI")
    else
      verbose_message("disabling UI")
    end
    @menu_eqe.setEnabled(en)
    @tool_bar.setEnabled(en)
    # re-enabling does not work in some configurations,
    # (weird), so doing it by hand
    for i in @tool_bar.children
      begin i.setEnabled(en) rescue () end
    end
  end

  def in_case_of_problem
    if not @problem_dialog
      @problem_dialog = Qt::Dialog.new @main_window
      contents = Qt::TextEdit.new @problem_dialog
      contents.setReadOnly true
      layout = Qt::VBoxLayout.new @problem_dialog
      layout.addWidget contents
      contents.setAlignment Qt::AlignJustify
      contents.insertHtml "<b>Please send comments, suggestions, questions and bug reports to <a href=\"mailto:rlehy@free.fr\">rlehy@free.fr</a> (that's me, Ronan). They are all very welcome!</b><br /><br />"
      contents.insertHtml '<b>If you report a problem, please paste the following (long, but useful) information.</b><br/><br/><br/>'
      contents.insertHtml Programs.debug_info
      contents.moveCursor Qt::TextCursor::Start
      @problem_dialog.resize 600, 400
    end
    @problem_dialog.show
    @problem_dialog.raise
    @problem_dialog.activateWindow
  end

  def about()
    if not @about_dialog
      @about_dialog = Qt::Dialog.new(@main_window)
      name = "<b>#{$NAME}: LaTeX Equation Editor, version #{$VERSION}</b>";
      copyright = "copyright (c) Ronan Le Hy, 2004-2008";
      description = "<br />An equation editor that produces images in various formats, with drag and drop support.";
      license = "<br />
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; version 2 of the
License only.<br />

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.<br />

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.,
59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
"
      about_text = "#{name}
#{copyright}

#{description}

#{license}"
      label = Qt::TextBrowser.new(@about_dialog)
      #label.setAlignment(Qt::AlignJustify)
      for p in [name, copyright, description, license]
        label.setAlignment(Qt::AlignJustify)
        label.insertHtml("<p>" + p.gsub("\n", ' ') + "</p><br />") 
      end
      label.readOnly = true
      layout = Qt::VBoxLayout.new @about_dialog
      layout.addWidget label
      # @about_dialog.setCaption("About #{$NAME}")
      @about_dialog.resize(Qt::Size.new(500, 400)) 
    end
    @about_dialog.show
    @about_dialog.raise
    @about_dialog.activateWindow
  end

  def init_connections(main_window)
    Qt::Object.connect(@action_redo, SIGNAL('triggered()'),
                       @input_zone, SLOT('redo()'))
    Qt::Object.connect(@action_undo, SIGNAL('triggered()'),
                       @input_zone, SLOT('undo()'))
    Qt::Object.connect(@action_clear, SIGNAL('triggered()'),
                       @input_zone, SLOT('clear()'))
    Qt::Object.connect(@action_quit, SIGNAL('triggered()'),
                       main_window, SLOT('close()'))
    Qt::Object.connect(@action_about, SIGNAL('triggered()'),
                       self, SLOT('about()'))
    Qt::Object.connect(@action_problem, SIGNAL('triggered()'),
                       self, SLOT('in_case_of_problem()'))
    Qt::Object.connect(@action_library, SIGNAL('triggered()'),
                       self, SLOT('open_library()'))
    Qt::Object.connect(@action_open, SIGNAL('triggered()'),
                       self, SLOT('open_equation()'))
    Qt::Object.connect(@action_bigger, SIGNAL('triggered()'),
                       @option_pane, SLOT('bigger()'))
    Qt::Object.connect(@action_smaller, SIGNAL('triggered()'),
                       @option_pane, SLOT('smaller()'))
    Qt::Object.connect(self, SIGNAL('destroyed()'),
                       self, SLOT('save_cosmetic()'))
    Qt::Object.connect(@status_show_log_button, SIGNAL('clicked()'),
                       self, SLOT('show_log()'))
  end

  def dnd(format, as_data)
    drag = Qt::Drag.new self
    
    @updater.generate('png', @option_pane.options) do |f|
      drag.setPixmap(Qt::Pixmap.new(f))
    end
    #drag.setPixmap(Qt::Pixmap.new(@generator.file('png', @option_pane.options)))

    @updater.generate(format, @option_pane.options) do |file|
      #file = @generator.file(format, @option_pane.options)
      
      if as_data
        data = mime_data_copy(file, 'image/' + format)
      else
        file = Temp.install(File.dirname(file), File.basename(file), EqePath::Library)
        data = mime_data_link(file)
      end
      
      drag.setMimeData(data)
      
      dropAction = drag.exec(Qt::CopyAction | Qt::LinkAction)
      
      if dropAction == Qt::MoveAction or
          dropAction == Qt::CopyAction or
          dropAction == Qt::LinkAction
        return true
      else
        unless as_data
          begin
            # puts "Drag and drop failed, deleting file #{file}"
            File.unlink file
          rescue => e
            log_exception(e)
            # warn "Cannot unlink file '#{file}'."
          end
        end
        return nil
      end
    end
  end

  def signal_error(e)
#     @status_bar.clearMessage
#     @status_show_log_button.setMaximumWidth(@status_bar.width - 20)
#     @status_show_log_button.setText e
#     @status_show_log_button.show
    verbose_message("UI error: " + e)
    e = truncate_string e, 50
    @status_bar.showMessage e
    @status_show_log_button.show
  end

  def signal_success(e)
    verbose_message("UI success: " + e)
    @status_bar.showMessage e
    @status_show_log_button.hide
  end

  def signal_status(e)
    verbose_message("UI status: " + e)
    @status_bar.showMessage e
    @status_show_log_button.hide
  end

  def clear_status
    @status_bar.clearMessage
    @status_show_log_button.hide
  end

  def save_as(format)
    save_as = Qt::FileDialog.getSaveFileName(self, "Save image in #{format} format as...", @save_dir)
    if save_as
      @save_dir = File.dirname save_as
      begin
        @generator.file_async(format, @option_pane.options, @log_window) do |file|
          File.copy(file, save_as)
        end
        signal_success "Equation saved in #{format} format as #{save_as}."
      rescue => e
        log_exception(e)
        @log_window.add e, 'red'
        signal_error 'Error saving file.'
      end
    end
  end

  def open_equation()
    open = Qt::FileDialog.getOpenFileName(self, "Open equation in PNG file...", @save_dir)
    if open
      @save_dir = File.dirname open
      begin
        @option_pane.set_options(YAML.load(Png.get_metadata(open)))
        signal_success "Equation opened from file #{open}."
      rescue => e
        log_exception(e)
        @log_window.add e, 'red'
        signal_error "Error opening equation from file #{open}."
      end
    end
  end

  def open(format)
    begin
      @generator.file_async(format, @option_pane.options, @log_window) do |file|
        run_synchronous_process('/usr/bin/xdg-open', [file], @log_window)
        # edit_file(file, @log_window)
        signal_success "Equation opened in #{format} format."
      end
    rescue => e
      log_exception(e)
      @log_window.add e, 'red'
      signal_error "Error opening equation in #{format} format."
    end
  end
end

def main
  parser = OptionParser.new do |opts|
    opts.banner = "This is #{$NAME} version #{$VERSION}, an equation editor (Ronan Le Hy, 2004-2008).\nusage: #{$NAME.downcase} [options]"
    opts.on('-v', '--verbose', "Print messages to stdout about what #{$NAME} is doing.") do
      $verbose = true
    end
    opts.on('--version', 'Shows the program version and exits.') do
      puts $VERSION
      exit
    end
    opts.on('-d', '--debug', 'Prints debug information.') do
      app = Qt::Application.new(ARGV)
      puts Programs.debug_info
      exit
    end
    opts.on_tail('-h', '--help', 'Shows this help.') do
      puts opts
      exit
    end
  end
  begin
    parser.parse!
  rescue => e
    puts "Error parsing command line: #{e}"
    puts parser
    exit(1)
  end

  ENV["LC_NUMERIC"] = "C"
  app = Qt::Application.new(ARGV)

  Temp.with_temp_dir do |working_dir|
    ui = Ui.new working_dir
    begin
      ui.main_window.show
      app.exec
    ensure
      ui.save :cosmetic
    end
  end
end

main
