#!/usr/bin/env ruby
# ----------------------------------------------------------------------
# Copyright (C) 2022-2026 Geraldo Ribeiro <geraldo@intmain.io>
# ----------------------------------------------------------------------

#   # Introduction
#   
#   docmd is a tool to extract documentation from source code.
#   
#   ## Usage
#   
#   ```bash
#   intmain_docmd tipo arquivo_fonte arquivo_markdown
#   ```
#   
#   where `type` is one of the following options:
#   
#   * `bash`
#   * `c++`
#   * `coffee`
#   * `conf`
#   * `grdb`
#   * `makefile`
#   * `lua`
#   * `nginx`
#   * `python`
#   * `ruby`
#   * `rust`
#   * `scss`
#   * `systemd`
#   * `verilog`
#   * `vhdl`
#   * `vim`
#   * `yaml`
#   * `toml`
#   
#   If no argument is supplid the tool ends its execution.
#   
#{{{
if ARGV.empty?
  puts """
Gerador de documentação em formato Markdown

intmain_docmd tipo arquivo_fonte arquivo_markdown [stripped_filename]

tipo: vim, bash, python, ruby, coffee, makefile, grdb, nginx, scss, conf, c++, systemd, verilog, vhdl, yaml, rust, toml

Para alterar o comportamento da ferramenta utilize as seguintes variáveis de ambiente:

  DOCMD_DETAILS=false     desabilita a tag details
  DOCMD_TOC=false         desabilita a geração de sumário
  DOCMD_SHOW_SOURCE=false desabilita a identificação do arquivo fonte
  DOCMD_SHOW_BANNER=false desabilita o banner
  DOCMD_OVERRIDE_MODE=false desabilita a sobrescrita do arquivo
  """
  exit
end
#}}}
#   
#   ### Exemplo real
#   
#   A documentação deste projeto é mantida utilizando-se o `intmain_docmd`:
#   
#   ```bash
#   intmain_docmd ruby     bin/intmain_docmd               doc/10_Gerador_de_documentação_intmain_docmd.md
#   intmain_docmd bash     bin/intmain_resize_screenshots  doc/20_Redimensionador_de_screenshots.md
#   intmain_docmd bash     scripts/Common/intmain.bash     doc/20_Biblioteca_de_scripts_intmain.md
#   intmain_docmd bash     scripts/Common/taoker.bash      doc/21_Biblioteca_de_scripts_taoker.md
#   intmain_docmd bash     scripts/Common/common.bash      doc/30_Biblioteca_de_funções_bash.md
#   intmain_docmd makefile scripts/Common/Makefile.include doc/40_Biblioteca_de_alvos_make.md
#   ```
#   
#   ## Variáveis de controle
#   
#   * `DOCMD_DETAILS`: ativa/desativa o encapsulamento dos trecho de código
#   * `DOCMD_TOC`: ativa/desativa a geração do índice
#   * `DOCMD_SHOW_SOURCE`: exibe o nome do arquivo de origem
#   * `DOCMD_SHOW_BANNER`: exibe o banner no início do arquivo
#   
#{{{
use_detail  = ( ENV['DOCMD_DETAILS']     || ENV['INTMAIN_DOCMD_DETAILS']     || 'true' ).match( /(true|t|yes|y)/i )
use_toc     = ( ENV['DOCMD_TOC']         || ENV['INTMAIN_DOCMD_TOC']         || 'true' ).match( /(true|t|yes|y)/i )
show_source = ( ENV['DOCMD_SHOW_SOURCE'] || ENV['INTMAIN_DOCMD_SHOW_SOURCE'] || 'true' ).match( /(true|t|yes|y)/i )
show_banner = ( ENV['DOCMD_SHOW_BANNER'] || ENV['INTMAIN_DOCMD_SHOW_BANNER'] || 'true' ).match( /(true|t|yes|y)/i )
override_mode = ( ENV['DOCMD_OVERRIDE_MODE'] || ENV['INTMAIN_DOCMD_OVERRIDE_MODE'] || 'true' ).match( /(true|t|yes|y)/i )
#}}}

if override_mode
  file_open_mode = "w"
else
  file_open_mode = "a"
end

script_language           = ARGV[0]
source_filename           = ARGV[1]
output_filename           = ARGV[2]
stripped_filename         = ARGV[3]
# All diagrams source will be writen on same pu file
output_plantuml_filename  = nil
# Each diagram has its own pikchr file
output_pikchr_filename    = nil
output_requisite_filename = nil
output_plantuml           = nil
start_plantuml_re         = /@start(uml|wbs) (\S+.png) ?(.*)?/
end_plantuml_re           = /@end(uml|wbs)/
start_pikchr_re           = /@startpikchr (\S+\.pikchr) ([^ ]+) (.*)/
end_pikchr_re             = /@endpikchr/
start_requisite_re        = /REQUISITO (\S+) -> (.*)/
end_requisite_re          = /REQUISITO/
in_code_section           = false
in_plantuml_section       = false
in_pikchr_section         = false
in_requisite_section      = false

line_match_re = case script_language
                when "bash", "python", "ruby", "coffee", "makefile", "nginx", "grdb", "conf", "systemd", "toml" then /^\#   (.*)/
                when "yaml" then /\#   (.*)/
                when "vim" then /^"   (.*)/
                when "vhdl", "lua" then /^--- (.*)/
                when "c++", "scss", "rust", "verilog" then /^\s*\/\/-- (.*)/
                else
                  abort( "#{script_language} language is not supported" )
                end
# after code reformat the space at the end is suppressed
blank_line_match_re = case script_language
                      when "c++", "scss", "rust", "verilog" then /^\s*\/\/--$/
                      else
                        /no-regular-expression-for-blank-line/
                      end

open_code_re = case script_language
               when "bash", "python", "ruby", "coffee", "makefile", "nginx", "grdb", "conf", "systemd", "toml" then /^\#{{{(.*)/
               when "yaml" then /\#{{{(.*)/
               when "vim" then /^"{{{(.*)/
               when "vhdl", "lua" then /^--{{{(.*)/
               when "c++", "scss", "rust", "verilog" then /^\s*\/\/{{{(.*)/
               else
                  abort( "#{script_language} language is not supported" )
               end

close_code_re = case script_language
                when "bash", "python", "ruby", "coffee", "makefile", "nginx", "grdb", "conf", "systemd", "toml" then /^\#}}}/
                when "yaml" then /\#}}}/
                when "vim" then /^"}}}/
                when "vhdl", "lua" then /^--}}}/
                when "c++", "scss", "rust", "verilog" then /^\s*\/\/}}}/
                else
                  abort( "#{script_language} language is not supported" )
                end

def make_link(title)
  anchor = title
  anchor = anchor.gsub( /[()\.`]/, "" ) # remove caracteres ignorados pelo gogs
  anchor = anchor.gsub( " ", "-" ).downcase

  "[#{title}](\##{anchor})"
end
#   
#   ## Callout numbers
#   
#   Para referenciar comandos use itens numerados, p. e. `((``1))`, para indicar
#   uma linha no meio do código.
#   
#{{{ruby alternative syntax
def expand_shortcuts(text)
  callout_numbers = { "((1))" => "❶", "((2))" => "❷", "((3))" => "❸",
                      "((4))" => "❹", "((5))" => "❺", "((6))" => "❻",
                      "((7))" => "❼", "((8))" => "❽", "((9))" => "❾",
                      "((10))" => "❿", "((11))" => "⓫", "((12))" => "⓬",
                      "((13))" => "⓭", "((14))" => "⓮", "((15))" => "⓯",
                      "((16))" => "⓰", "((17))" => "⓱", "((18))" => "⓲",
                      "((19))" => "⓳", "((20))" => "⓴" }
  callout_numbers.each{ |k,v| text = text.gsub( k, v ) }
  text
end
#}}}

unless stripped_filename.nil?
  stripped_file = File.open(stripped_filename, file_open_mode)
  File.readlines(source_filename).each do |line|
    unless line.match( line_match_re ) || line.match( open_code_re ) || line.match( close_code_re ) || line.match( blank_line_match_re )
      stripped_file << line
    end
  end
end

# Lê o arquivo inteiro uma vez para gerar o sumário
toc = ""
File.readlines(source_filename).each do |line|
  if m = line.match( line_match_re )
    mdline = m[1]
    if m = mdline.match( /^# (.*)/ )
      toc += "* #{make_link(m[1])}\n"
    elsif m = mdline.match( /^## (.*)/ )
      toc += "  * #{make_link(m[1])}\n"
    elsif m = mdline.match( /^### (.*)/ )
      toc += "      * #{make_link(m[1])}\n"
    end

    # Verificar se utiliza plantuml
    if mdline.match( start_plantuml_re )
      output_plantuml_filename = output_filename.gsub( ".md", ".pu" )
    end

    if mdline.match( start_requisite_re )
      output_requisite_filename = output_filename.gsub( ".md", "_req.md" );
    end
  end
end

#   
#   ## Inclusão de diagramas com plantuml
#   
#   Os trechos em `plantuml` a seguir serão substituídos pela imagem do diagrama.
#   
#   * `@``startuml nome-do-arquivo-plantuml.png url-opcional`
#   * `@``startwbs nome-do-arquivo-plantuml.png url-opcional`
#   
#   Imagem:
#   
#   @startuml images/nome-do-arquivo-plantuml.png
#   (First usecase)
#   (Another usecase) as (UC2)
#   usecase UC3
#   usecase (Last\nusecase) as UC4
#   @enduml
#   
#   Imagem:
#   
#   @startuml images/outro-nome-do-arquivo-plantuml.png
#   (First usecase 2)
#   (Another usecase 2) as (UC2)
#   usecase UC3
#   usecase (Last\nusecase 2) as UC4
#   @enduml
#   
#   Link para imagem:
#    
#   @startwbs images/exemplo-wbs-plantuml.png https://git.intmain.io/Intmain/docmd/raw/master/images/exemplo-wbs-plantuml.png
#   * Business Process Modelling WBS
#   ** Launch the project
#   *** Complete Stakeholder Research
#   *** Initial Implementation Plan
#   ** Design phase
#   *** Model of AsIs Processes Completed
#   **** Model of AsIs Processes Completed1
#   **** Model of AsIs Processes Completed2
#   *** Measure AsIs performance metrics
#   *** Identify Quick Wins
#   ** Complete innovate phase
#   @endwbs
#   
#   ## Inclusão de diagramas com pikchr
#
#   @startpikchr images/nome-do-arquivo-pikchr.pikchr /til/nome-do-arquivo-pikchr.svg The image title goes here
#   arrow right 200% "Markdown" "Source"
#   box rad 10px "Markdown" "Formatter" "(markdown.c)" fit
#   arrow right 200% "HTML+SVG" "Output"
#   arrow <-> down 70% from last box.s
#   box same "Pikchr" "Formatter" "(pikchr.c)" fit
#   @endpikchr
#
#   ## Inclusão de arquivo
#   
#   Um arquivo pode ser incluído como bloco de código na documentação.
#   
#   `[``DOCMD_INCLUDE_FILE cpp src/main.cpp]`
#   
#   [DOCMD_INCLUDE_FILE cpp src/main.cpp]
#   
#   Parágrafo após a inclusão do arquivo.
#
#   `[``DOCMD_INCLUDE_PARTIAL_FILE 6 +3 ruby bin/intmain_docmd]`
#   
#   [DOCMD_INCLUDE_PARTIAL_FILE 6 +3 ruby bin/intmain_docmd]
#   
#   `[``DOCMD_INCLUDE_PARTIAL_FILE 6 9 ruby bin/intmain_docmd]`
#   
#   [DOCMD_INCLUDE_PARTIAL_FILE 6 9 ruby bin/intmain_docmd]
#   
#   Parágrafo após a inclusão do arquivo.
#   
#   ## Inclusão de arquivo markdown
#   
#   `[``INCLUDE filename.md]`
#   
#   [INCLUDE filename.md]`
#   
#   ## Importação de arquivos fontes
#   
#   `[``IMPORT imported1.rb]`
#   
#   [IMPORT imported1.rb]
#   

output_plantuml  = File.open(output_plantuml_filename, file_open_mode)  unless output_plantuml_filename.nil?
output_requisite = File.open(output_requisite_filename, file_open_mode) unless output_requisite_filename.nil?
output_pikchr  = File.open(output_pikchr_filename, file_open_mode)  unless output_pikchr_filename.nil?

# Main markdown file
File.open(output_filename, file_open_mode) do |output|

  if show_banner
    output << "[//]: <> (Documentation generated by intmain_docmd)\n"
  end

  if output_requisite
    if show_banner
      output_requisite << "[//]: <> (Documentation generated by intmain_docmd)\n"
    end
    output_requisite << "# Requirement List\n\n"
  end

  if output_plantuml
    if show_banner
      output_plantuml << "[//]: <> (Documentation generated by intmain_docmd)\n"
    end
  end

  if show_source
    output << "Source file: `#{source_filename}`\n\n"
  end

  lines = []

  File.readlines(source_filename).each do |line0|
    if m0 = line0.match( /\[IMPORT ([^ ]+)\]/ )
      imported_file_1 = m0[1]
      if File.exist?(imported_file_1)
        File.readlines(imported_file_1).each do |line1|
          if m1 = line1.match( /\[IMPORT ([^ ]+)\]/ )
            imported_file_2 = m2[1]
            if File.exist?(imported_file_2)
              File.readlines(imported_file_2).each do |line2|
                lines.push(line2)
              end
            else
              print "#{imported_file_2} not found!"
            end
          else
            lines.push(line1)
          end
        end
      else
        print "#{imported_file_1} not found!"
      end
    else
      lines.push(line0)
    end
  end

  #File.readlines(source_filename).each do |line|
  lines.each do |line|
    if m = line.match( line_match_re )
      mdline = m[1]

      mdline = expand_shortcuts( mdline )

      if m2 = mdline.match( start_pikchr_re )
        output_pikchr_filename = m2[1]
        svgFilename = output_pikchr_filename.gsub( ".pikchr", ".svg" )
        url = m2[2]
        title =m2[3]
        if url.empty?
          output << "![#{title}](#{svgFilename})\n"
        else
          output << "![#{title}](#{url})\n"
        end
        output_pikchr  = File.open(output_pikchr_filename, file_open_mode)
        in_pikchr_section = true
        next
      elsif in_pikchr_section && mdline.match( end_pikchr_re )
        in_pikchr_section = false
        output_pikchr.close
        next
      elsif m2 = mdline.match( start_plantuml_re )
        type = m2[1]
        pngFilename = m2[2]
        url = m2[3]
        if url.empty?
          output << "![UML](#{pngFilename})\n"
        else
          output << "![UML](#{url})\n"
        end
        #output_plantuml << mdline << "\n"
        output_plantuml << "@start#{type} #{pngFilename}\n"
        in_plantuml_section = true
        next
      elsif in_plantuml_section && mdline.match( end_plantuml_re )
        in_plantuml_section = false
        output_plantuml << mdline << "\n"
        output_plantuml << "\n"
        next
      elsif m2 = mdline.match( start_requisite_re )
        reqId = m2[1]
        reqClass = m2[2]
        output_requisite << "## Requirement " << reqId << " (" << reqClass << ")\n\n"
        in_requisite_section = true
        next
      elsif in_requisite_section && mdline.match( end_requisite_re )
        output_requisite << "\n"
        in_requisite_section = false
        next
      elsif mdline.match( /^\[TOC\]/ )
        if use_toc
          output << "## Summary\n\n"
          output << toc << "\n"
        end
      elsif m2 = mdline.match( /\[INCLUDE ([^ ]+.md)\]/ )
        include_filename=m2[1]
        File.readlines(include_filename).each do |include_line|
          output << include_line
        end
        next
      elsif m2 = mdline.match( /\[IMDOC_INCLUDE_FILE ([^ ]+) (.*)\]/ )
        include_type=m2[1]
        include_filename=m2[2]
        output << "\n"
        output << "```" << include_type << "\n"
        if File.exist?(include_filename)
          File.readlines(include_filename).each do |include_line|
            output << include_line
          end
        else
          output << "File " << include_filename << " not found!"
        end
        output << "\n```\n"
        next
      elsif m2 = mdline.match( /\[DOCMD_INCLUDE_FILE ([^ ]+) (.*)\]/ )
        include_type=m2[1]
        include_filename=m2[2]
        output << "\n"
        output << "```" << include_type << "\n"
        if File.exist?(include_filename)
          File.readlines(include_filename).each do |include_line|
            output << include_line
          end
        else
          output << "File " << include_filename << " not found!"
        end
        output << "\n```\n"
        next
      elsif m2 = mdline.match( /\[IMDOC_INCLUDE_PARTIAL_FILE ([0-9]+) ([0-9]+) ([^ ]+) (.*)\]/ )
        initial_line     = m2[1].to_i
        number_of_lines  = m2[2].to_i
        include_type     = m2[3]
        include_filename = m2[4]
        output << "\n"
        output << "```" << include_type << "\n"
        File.readlines(include_filename).each do |include_line|
          # Skip initial lines
          if initial_line > 1
            initial_line -= 1
            next
          end
          if number_of_lines > 0
            number_of_lines -= 1
            output << include_line
          end
        end
        output << "```\n"
        next
      elsif m2 = mdline.match( /\[DOCMD_INCLUDE_PARTIAL_FILE ([0-9]+) \+([0-9]+) ([^ ]+) (.*)\]/ )
        initial_line     = m2[1].to_i
        number_of_lines  = m2[2].to_i
        include_type     = m2[3]
        include_filename = m2[4]
        output << "\n"
        output << "```" << include_type << "\n"
        File.readlines(include_filename).each do |include_line|
          # Skip initial lines
          if initial_line > 1
            initial_line -= 1
            next
          end
          if number_of_lines > 0
            number_of_lines -= 1
            output << include_line
          end
        end
        output << "```\n"
        next
      elsif m2 = mdline.match( /\[DOCMD_INCLUDE_PARTIAL_FILE ([0-9]+) ([0-9]+) ([^ ]+) (.*)\]/ )
        initial_line     = m2[1].to_i
        final_line       = m2[2].to_i
        if final_line < initial_line
          final_line = initial_line + final_line
        end
        number_of_lines  = final_line - initial_line
        include_type     = m2[3]
        include_filename = m2[4]
        output << "\n"
        output << "```" << include_type << "\n"
        File.readlines(include_filename).each do |include_line|
          # Skip initial lines
          if initial_line > 1
            initial_line -= 1
            next
          end
          if number_of_lines > 0
            number_of_lines -= 1
            output << include_line
          end
        end
        output << "```\n"
        next
      end

      if in_plantuml_section
        output_plantuml << mdline << "\n"
      elsif in_requisite_section
        output_requisite << mdline << "\n"
      elsif in_pikchr_section
        output_pikchr << mdline << "\n"
      else
        output << mdline << "\n"
      end

    elsif blank_line_match_re && m = line.match( blank_line_match_re )
      output << "\n"
      next
    elsif m = line.match( open_code_re )
      in_code_section = true
      code_config = m[1]
      code_config = script_language if code_config.empty?
      code_config = 'cpp' if code_config == 'c++'

      output << "<details>\n<summary>Details</summary>\n<p>\n" if use_detail
      output << "\n```#{code_config}\n"

      next
    elsif m = line.match( close_code_re )
      in_code_section = false
      output << "```\n\n"
      output << "</p>\n</details>\n" if use_detail
    end
    # É necessário ter uma linha em branco após o fechamento de summary e details

    if in_code_section
      output << expand_shortcuts( line )
    end
  end
end
