module Asciidoctor
  module Standoc
    class PlantUMLBlockMacroBackend
      # https://stackoverflow.com/questions/2108727/which-in-ruby-checking-if-program-exists-in-path-from-ruby
      def self.plantuml_installed?
        cmd = "plantuml"
        exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
        ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
          exts.each do |ext|
            exe = File.join(path, "#{cmd}#{ext}")
            return exe if File.executable?(exe) && !File.directory?(exe)
          end
        end
        raise "PlantUML not installed"
        nil
      end

      def self.run umlfile, outfile
        system "plantuml #{umlfile.path}" or (warn $? and return false)
        i = 0
        until !Gem.win_platform? || File.exist?(outfile) || i == 15
          sleep(1)
          i += 1
        end
        File.exist?(outfile)
      end

      # if no :imagesdir: leave image file in plantuml
      # sleep need for windows because dot works in separate process and
      # plantuml process may finish earlier then dot, as result png file
      # maybe not created yet after plantuml finish
      def self.generate_file parent, reader
        localdir = Utils::localdir(parent.document)
        imagesdir = parent.document.attr('imagesdir')
        umlfile, outfile = save_plantuml parent, reader, localdir
        run(umlfile, outfile) or raise "No image output from PlantUML (#{umlfile}, #{outfile})!"
        umlfile.unlink

        path = Pathname.new(localdir) + (imagesdir || "plantuml")
        File.writable?(localdir) or raise "Destination path #{path} not writable for PlantUML!"
        path.mkpath
        File.writable?(path) or raise "Destination path #{path} not writable for PlantUML!"
        #File.exist?(path) or raise "Destination path #{path} already exists for PlantUML!"

        # Warning: metanorma/metanorma-standoc#187
        # Windows Ruby 2.4 will crash if a Tempfile is "mv"ed.
        # This is why we need to copy and then unlink.
        filename = File.basename(outfile.to_s)
        FileUtils.cp(outfile, path) && outfile.unlink

        imagesdir ? filename : File.join(path, filename)
      end

      def self.save_plantuml parent, reader, localdir
        src = reader.source
        reader.lines.first.sub(/\s+$/, "").match /^@startuml($| )/ or
          src = "@startuml\n#{src}\n@enduml\n"
        /^@startuml (?<fn>[^\n]+)\n/ =~ src
        Tempfile.open(["plantuml", ".pml"], :encoding => "utf-8") do |f|
          f.write(src)
          [f, File.join(File.dirname(f.path),
                        (fn || File.basename(f.path, ".pml")) + ".png")]
        end
      end

      def self.generate_attrs attrs
        through_attrs = %w(id align float title role width height alt).
          inject({}) do |memo, key|
          memo[key] = attrs[key] if attrs.has_key? key
          memo
        end
      end
    end

    class PlantUMLBlockMacro < Asciidoctor::Extensions::BlockProcessor
      use_dsl
      named :plantuml
      on_context :literal
      parse_content_as :raw

      def abort(parent, reader, attrs, msg)
        warn msg
        attrs["language"] = "plantuml"
        create_listing_block parent, reader.source, attrs.reject { |k, v| k == 1 }
      end

      def process(parent, reader, attrs)
        begin
          PlantUMLBlockMacroBackend.plantuml_installed?
          filename = PlantUMLBlockMacroBackend.generate_file(parent, reader)
          through_attrs = PlantUMLBlockMacroBackend.generate_attrs attrs
          through_attrs["target"] = filename
          create_image_block parent, through_attrs
        rescue StandardError => e
          abort(parent, reader, attrs, e.message)
        end
      end
    end
  end
end