#!/System/Library/Frameworks/Ruby.framework/Versions/Current/usr/bin/ruby

require 'shellwords'
require 'fileutils'
require 'cgi'
require 'rexml/document'
require 'tempfile'
require 'open3'

$scriv_version = 2
$comments = {}

def class_exists?(class_name)
  klass = Module.const_get(class_name)
  begin
    klass.is_a?(Class)
  rescue NameError
    false
  end
end

if class_exists? 'Encoding'
  Encoding.default_external = Encoding::UTF_8 if Encoding.respond_to?('default_external')
  Encoding.default_internal = Encoding::UTF_8 if Encoding.respond_to?('default_internal')
end

class String
  def hexlify
    split("").collect { |c|
      c[0].to_s(16)
    }.join
  end

  def unhexlify
    scan(/../).collect { |c|
      c.to_i(16).chr
    }.join
  end

  def safename
    gsub(/[^\w.\-]/, "_")
  end

  def extract_images(folder, relative_root = nil)
    input = dup
    embed_re = /\\*\{\\\*\\shppict ?\{\\pict.*?filename (.*?)\}.*?picw(\d+).*?pich(\d+).*?\\(\w+)blip (.*?)\}\}(\\\n)?/m
    input.gsub!(embed_re) do |_m|
      name = Regexp.last_match(1)
      ext = Regexp.last_match(4)
      hex = Regexp.last_match(5)
      newline = Regexp.last_match(6) ? "\\\n\\\n" : "\\\n"
      # width = $2
      # height = $3

      base_folder = File.expand_path(folder)
      target = File.join(base_folder, "#{name}.#{ext}".safename)

      FileUtils.mkdir_p(base_folder)

      File.open(target, 'w+') do |f|
        f.binmode
        f.puts hex.unhexlify
      end

      if relative_root
        target = File.expand_path(target).sub(%r{^#{relative_root}/}, '')
      end

      "![#{name}](#{target.gsub(/ /, '%20')})#{newline}"
    end
    input
  end

  def convert_linked_images(relative_root = nil)
    input = dup
    image_re = /\\*\{\$SCRImageLink\[w:(\d+);h:(\d+)\]=(.*?)\\\}(\\\n)?/
    input.gsub!(image_re) do |_m|
      path = Regexp.last_match(3).strip
      name = File.basename(path).sub(/\.\w{3,4}/, '')
      newline = Regexp.last_match(4) ? "\\\n\\\n" : "\\\n"
      # width = $1
      # height = $2

      if relative_root
        path = File.expand_path(path).sub(%r{^#{relative_root}/}, '')
      end

      "![#{name}](#{path.gsub(/ /, '%20')})#{newline}"
    end
    input
  end

  def fix_hyperlinks
    output = dup
    # if $scriv_version == 3
    #   output.gsub!(%r{\{\\field\{\\\*\\fldinst\{HYPERLINK "scrivcmt://(.*?)"\}\}\{\\fldrslt .*?\}\}}) do |_m|
    #     lm = Regexp.last_match
    #     uuid = lm[1]
    #     if $comments.key?(uuid)
    #       %{((((COMMENT))))#{$comments[uuid]}((((/COMMENT))))}
    #     end
    #   end
    # end

    output.gsub!(/\[(.*?)\]\(\{\\field\{\\\*\\fldinst\{HYPERLINK "(.*?)"\}\}\{\\fldrslt .*?}}\)/, '[\1](\2)')
    output.gsub!(/^ {0,3}\[(.*?)\]: +\{\\field\{\\\*\\fldinst\{HYPERLINK "(.*?)"\}\}\{\\fldrslt .*?}}/, '[\1]: \2')

    output
  end

  def fix_lists
    gsub(/^(\t+)([\-*+])/) do |_m|
      lm = Regexp.last_match
      pre = lm[1]
      "    " * pre.length + lm[2]
    end
  end

  def double_indent
    gsub(/  /, '    ')
  end

  def markdownify
    tables = {}
    table_index = 0
    input = dup
    input.sub!(%r{(?:.|\n)*?<body>((?:.|\n)*)</body>(?:.|\n)*}, '\1')
    input.gsub!(%r{<span class="Apple-tab-span">.*?</span>}, ' ')
    input.gsub!(/\u00A0/, "@SPACE@")
    # input.gsub!(/<span class="Apple-converted-space">(.*?)<\/span>/) {|m|
    #   match = Regexp.last_match
    #   " " * match[1].length
    # }
    input.gsub!(%r{<span class="s2">.*?</span>}, '')
    input.gsub!(/•/, '')
    input.gsub!(/(?mi)&lt;\$Scr_H::1&gt;/, '<h2>')
    input.gsub!(/(?mi)&lt;!\$Scr_H::1&gt;/, '</h2>')
    input.gsub!(/&lt;!?\$Scr_Ps::0&gt;/, '')
    input.gsub!(/&lt;!?\$Scr_Cs::\d&gt;/, '')
    input.gsub!(/[“”]/, '"')
    input.gsub!(/[’‘]/, "'")
    input.gsub!(%r{</?span.*?>}, '')
    input.gsub!(/&amp;/, '&')
    input.gsub!(/&nbsp;/, ' ')
    input.gsub!(%r{<table .*?class=".*?">(.*?)</table>}m) { |m|
      # m.gsub!(/\s*<\/?t.*?>/,'')
      # puts '<blockquote class="mkscrivtable">' + m.markdownify + '</blockquote>'
      # "\n$@$@$@$@\n" + m + "\n!@!@!@!@\n"
      table = m.gsub(%r{<p class="p.*?">(.*?)</p>}, '\1')
      tables[table_index.to_s] = table
      out = "@@@TABLE:#{table_index}@@@"
      table_index += 1
      out
    }

    input.gsub!(/(?mi)\{\\Scrv_annot(?:[\s\S]*?)\\text=([\s\S]*?)\\end_Scrv_annot\}/,
%{((((ANNOTATION))))\\1((((/ANNOTATION))))})

    # FIXME "mathjax" escaping
    input.gsub!(/\\/, "\\\\\\")

    locations = %w[
      /System/Library/Frameworks/Python.framework/Versions/Current/bin/python
      /Library/Frameworks/Python.framework/Versions/3.7/bin/python3
      /usr/bin/python3
      /usr/local/bin/python3
      /opt/homebrew/bin/python3
      /opt/anaconda3/bin/python3
    ]
    python  = nil
    locations.each do |path|
      python = path
      break if File.exist?(path)
    end

    python = `which python`.strip unless python && File.exist?(python)
    python = `which python3`.strip unless python && File.exist?(python)

    if python.nil? || !File.exist?(python)
      puts "Error locating Python executable"
      Process.exit
    end

    version = get_python_version(python)
    # $stderr.puts "Using Python #{version} at #{python}"
    html2text = File.join(File.dirname(__FILE__), version == 3 ? 'html2text3.py' : 'html2text2.py')
    result = `echo #{Shellwords.escape(input)} | #{python} #{Shellwords.escape(html2text)} -S`

    result.gsub!(/(?mi)(#+) \*\*(.*?)\*\*/, '\1 \2')
    result.gsub!(/(?mi)^(#+.*?\n)(?=\S)/, "\\1\n")
    result.gsub!(/(?mi)^(>+[\s\S]*(?=\n\n))/, "\n\\1\n")
    result.gsub!(/(?mi)^ +(?=[-*+])/) { |m|
      m.double_indent
    }
    result.gsub!(/@@@TABLE:(\d+)@@@/) { |_match|
      m = Regexp.last_match
      tables[m[1]]
    }
    result.gsub!(/\\([*_><])/, '\1')
    result.gsub!(/@SPACE@/, ' ')

    result.gsub!(/\[(.*?)\]\(scrivcmt:\/\/(.*?)\)/) do |_match|
      lm = Regexp.last_match
      if $scriv_version == 3
        uuid = lm[2]
        if $comments.key?(uuid)
          %(#{lm[1]}<span class="mkscriv_annotation">#{$comments[uuid]}</span>)
        else
          lm[1]
        end
      else
        lm[1]
      end
    end
    result.gsub!(%r{(?mi)\(\(\(\(COMMENT\)\)\)\)(.*?)\(\(\(\(/COMMENT\)\)\)\)},
'<span class="mkscriv_annotation">\1</span>')
    result.gsub!(%r{(?mi)\(\(\(\(ANNOTATION\)\)\)\)(.*?)\(\(\(\(/ANNOTATION\)\)\)\)},
'<span class="mkscriv_annotation inline">\1</span>')
    # Fix tables with double spacing
    result.gsub!(/(?mi)(\[.*?\]\n+)?(^.*?\|.*?\n+){2,}(\[.*?\]\n+)?/) { |m|
      "\n" + m.gsub(/\n+/, "\n") + "\n"
    }
    # Fix math with double spacing
    result.gsub!(/(?mi)^(\$\$.*?\$\$|\\\\\[.*?\\\\\])/) { |m|
      "\n" + m.gsub(/\n+/, "\n") + "\n"
    }

    result.to_s
  end
end

def get_python_version(exe)
  stdin, stdout, stderr, wait_thr = Open3.popen3(exe, '--version')
  out = stdout.gets(nil)
  stdout.close
  err = stderr.gets(nil)
  stderr.close
  if out.nil? || out.empty?
    ver = err.match(/Python (\d)/)
    ver ? ver[1].to_i : 3
  else
    ver = out.match(/Python (\d)/)
    ver ? ver[1].to_i : 3
  end
end

def gather_comments(cpath)
  comments = {}
  globpath = File.join(cpath, 'Files/Data/', '**', 'content.comments')
  Dir.glob(globpath).each { |path|
    doc = REXML::Document.new File.new(path)
    doc.elements.each('Comments/Comment') do |ele|
      id = ele.attributes['ID']
      contents = ele.text.sub(/<!\[CDATA\[(.*?)\]\]>/s, '\1')
      text = rtf_to_html(contents)
      comments[id] = text.markdownify.strip.gsub(/(\n\s*)+/, "<br><br>")
    end
  }
  comments
end

# scrivcompile.rb "path/to/doc.scriv" useheaders(true/false)

def get_children(ele, path, files, depth)
  ele.elements.each('*/BinderItem') do |child|
        # Ignore docs set to not include in compile
    includetag = REXML::XPath.first(child, "MetaData/IncludeInCompile")
        if !includetag.nil? && includetag.text == "Yes"
          id = if $scriv_version == 2
            child.attributes["ID"]
          else
            child.attributes["UUID"]
               end
          # passing type, would eventually use to control header/title output
          type = child.attributes["Type"]
          elArray = child.elements.to_a
          if elArray.length > 0
            title = elArray[0].text
            title = title.force_encoding('utf-8') if title.respond_to?('force_encoding')
          else
            title = ''
          end
          file = if $scriv_version == 2
            File.expand_path("#{path}/Files/Docs/#{id}.rtf")
          else
            File.expand_path("#{path}/Files/Data/#{id}/content.rtf")
                 end
          filepath = File.exist?(file) ? file : false
          files << { 'path' => filepath, 'title' => title, 'depth' => depth, 'type' => type }
        end
        get_children(child, path, files, depth + 1)
  end
end

def expand_scriv_path(scrivpath)
  scrivpath = File.expand_path(scrivpath)
  scrivx = File.basename(scrivpath.to_s, ".scriv") + ".scrivx"
  scrivxpath = File.join(scrivpath, scrivx)
  # handle cases where the package has been renamed after creation
  unless File.exist?(scrivxpath)
    scrivxpath = `ls -1 "#{scrivpath}/*.scrivx"|head -n 1`.strip
  end
  scrivxpath
end

def get_metadata(scrivxpath)
  doc = REXML::Document.new File.new(scrivxpath)
  props = REXML::XPath.first(doc.root, "ProjectProperties")
  properties = {}
  if props
    props.each_element_with_text { |e|
      properties[e.name.downcase] = e.text
    }
  end
  properties
end

# Take the path to the scrivener file and open the internal XML file.
def get_rtf_file_array(scrivpath, scrivxpath)
  files = []
  doc = REXML::Document.new File.new(scrivxpath)
  if doc.root.attributes['Version'].to_f > 1.5
    $scriv_version = 3
  end
  doc.elements.each('ScrivenerProject/Binder/BinderItem') do |ele|
    if ele.attributes['Type'] == "DraftFolder"
      get_children(ele, scrivpath, files, 1)
    end
  end

  files
end

def scrivParse(input, properties)
  if RUBY_VERSION.to_f > 1.9
    input = input.force_encoding('utf-8')
  end

  input.gsub!(/<\$(\w+)>/) { |_match|
    key = Regexp.last_match(1).downcase
    if properties.has_key?(key)
      properties[key]
    else
      Regexp.last_match(0)
    end
  }

  input.gsub!(/<!?\$Scr_.*?>/, '')
  input.gsub!(/^\s*\{\\Scrv_ps=(.*?)\\end_Scrv_ps\}\s*/m, %{\n<pre markdown="0"><code>\\1</code></pre>\n})
  input.gsub!(/\{\\Scrv_ps=(.*?)\\end_Scrv_ps\}/m, %{<code>\\1</code>})

  input.gsub!(/XXXBREAKXXX/, "\n\n<!--BREAK-->\n\n")
  input.gsub!(/XXXLINEBREAKXXX/, "  \n")
  input.gsub!(/XXXREALGRAFXXX/, "\n")
  footnotes = []
  fncount = 0
  input.gsub!(/\{\\Scrv_fn=(.*?)\\end_Scrv_fn\}/m) { |_match|
    # link = "[^fn" + rand(99999).to_s + "]"
    fncount += 1
    link = "[^fn" + fncount.to_s + "]"
    footnotes.push(link + ": " + Regexp.last_match(1))
    link
  }

  input + "\n\n" + footnotes.join("\n\n") + "\n\n"
end

# Something happened in Big Sur that stopped textutil from
# being able to read from STDIN (-stdin flag). This writes
# RTF content to a temp file and runs textutil on that.
def rtf_to_html(contents)
  html = ""
  file = Tempfile.new(['marked-scrivener-rtf', '.rtf'])
  begin
    file.write(contents)
    file.read
    html = `textutil -convert html -inputencoding UTF-8 -encoding UTF-8 -stdout "#{file.path}"`
 ensure
    file.close
    file.unlink
  end
  html
end

path = ARGV[0].sub(%r{/$}, '')
path = File.expand_path(path)
scrivxpath = expand_scriv_path(path)

useheaders = ARGV[1] && ARGV[1] == "true" ? true : false
cache_folder = ARGV[2] ? File.expand_path(ARGV[2]) : File.dirname(File.expand_path(path))
$comments = gather_comments(path)
files = get_rtf_file_array(path, scrivxpath)
properties = get_metadata(scrivxpath)
notetext = ""
files.each_with_index { |f, idx|
  if f['path']

    contents = IO.read(f['path']).force_encoding('utf-8')
    contents = contents.extract_images(cache_folder)
    contents = contents.convert_linked_images
    contents = contents.fix_hyperlinks
    contents = contents.fix_lists

    contents.gsub!(/\\page /, %{XXXBREAKXXX})
    contents.gsub!(/(\\uc0)?\\u8232 /, %{XXXLINEBREAKXXX})

    text = rtf_to_html(contents)

    note = text.markdownify
    if idx == 0 && f['title'].downcase == 'metadata'
      note.gsub!(/\n+/, "\n")
    end
  else
    note = ''
  end

  if useheaders && f['title'] != ''
    unless idx == 0 && f['title'].downcase == 'metadata'
      leader = "#"
      f['depth'].times { leader += "#" } unless f['depth'].nil?
      notetext += "#{leader} #{f['title']}\n\n"
    end
  end
  notetext += note + "\n\n"
}
# write the result to the preview file
# watch_note = File.new("#{sw_note}",'w')
# watch_note.puts notetext
# watch_note.close
# print scrivParse(notetext)
print scrivParse(notetext, properties).strip
