The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
-- Copyright 2016 Jeffrey Kegler
-- Permission is hereby granted, free of charge, to any person obtaining a
-- copy of this software and associated documentation files (the "Software"),
-- to deal in the Software without restriction, including without limitation
-- the rights to use, copy, modify, merge, publish, distribute, sublicense,
-- and/or sell copies of the Software, and to permit persons to whom the
-- Software is furnished to do so, subject to the following conditions:
--
-- The above copyright notice and this permission notice shall be included
-- in all copies or substantial portions of the Software.
--
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
-- THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
-- OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
-- ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
-- OTHER DEALINGS IN THE SOFTWARE.
--
-- [ MIT license: http://www.opensource.org/licenses/mit-license.php ]

local file_ids = {}

local bare_arg_count = 0
for arg_ix = 1,#arg do
    local this_arg = arg[arg_ix]
    if not this_arg:find("=") then
        bare_arg_count = bare_arg_count + 1
        if bare_arg_count == 1 then
            file_ids.stdin = this_arg
        elseif bare_arg_count == 2 then
            file_ids.stdout = this_arg
        else
            return nil, "Bad option: ", arg
        end
    else
        local id, val = this_arg:match("^([^=]+)%=(.*)") -- no space around =
        if not id or not val then
            return nil, "Bad option: ", this_arg
        end
        file_ids[id] = val
    end
end

file_ids.stdout = file_ids.stdout or '-'

local sections = {}
local outputs = {}
local current_section
local current_subsection
local current_prefix

local input_handle
if file_ids.stdin then
    local error_message
    input_handle, error_message = io.open(file_ids.stdin)
    if not input_handle then error(error_message) end
else input_handle = io.input() end

local line_number = 1
local lines = {}
for line in input_handle:lines() do
    lines[#lines+1] = line
end

local function form_section_name(line, words, offset)
    local section_name_table = {}
    if words[offset]:match("^!") then
        error("Section names starting in '!' are reserved\n"
           .. " Problem line: " .. line .. "\n"
        )
    end
    for word_ix = offset,#words do
        local word = words[word_ix]
        section_name_table[#section_name_table+1] = word
    end
    return table.concat(section_name_table, ' ');
end

local function section_end()
    current_section = nil
    current_subsection = nil
    -- I do not bother nil'ing the current_prefix
end

local function command_do(line, command_line_number)
    -- print('command_do', line, command_line_number)
    local prefix,command = line:match("^(%s*)--%s*luatangle:%s*(.*)$")
    if not prefix then return end
    if prefix:match('[^\32]') then
        error("Command prefix is not all spaces -- it must be\n"
            .. " Problem line: " .. line .. "\n"
        )
    end
    local words = {}
    for word in command:gmatch("[^%s]+") do
        words[#words+1] = word
    end
    if words[1] == 'section' then
        local section_name = form_section_name(line, words, 2)
        if sections[section_name] then
            error(
                'Line ' .. line_number .. ": Section '" .. section_name .. "' already exists\n"
                .. " Problem line: " .. line .. "\n"
            )
        end
        current_subsection = {
            first = command_line_number,
            last = command_line_number,
            prefix = prefix,
        }
        current_section = { current_subsection }
        sections[section_name] = current_section
        current_section.name = section_name
        current_prefix = prefix
        return
    elseif words[1] == 'section+' then
        local section_name = form_section_name(line, words, 2)
        current_section = sections[section_name]
        if not current_section then
            error("Section '" .. section_name .. "' does not exist\n"
                .. " Problem line: " .. line .. "\n"
            )
        end
        current_subsection = {
            first = command_line_number,
            last = command_line_number,
            prefix = prefix,
        }
        current_prefix = prefix
        current_section[#current_section + 1] = current_subsection
        return
    elseif words[1] == 'write' then
        if #words < 3 then
            error("Malformed output command\n"
                .. " Problem line: " .. line .. "\n"
            )
        end
        local output_fileid = words[2]
        local section_name = form_section_name(line, words, 3)
        local output_section = sections[section_name]
        if not output_section then
            error("output command to non-existent section\n"
                .. " Problem line: " .. line .. "\n"
            )
        end
        if outputs[section_name] then
            error("duplicate output command\n"
                .. " Second command is: " .. line .. "\n"
            )
        end
        local output_filename
        output_filename = file_ids[output_fileid] or output_fileid
        outputs[section_name] = output_filename
        return
    end

    if not current_section then
        error("Line " .. line_number .. ": command found outside of a section\n"
            .. "  It might be valid section command, but found outside of a section,\n"
            .. "  or it might simply an invalid command\n"
            .. "  Problem line: " .. line .. "\n"
        )
    end
    if #words == 2 and words[1] == 'end' and words[2] == 'section' then
        section_end()
        return
    elseif words[1] == 'insert' then
        -- At this point, do nothing
        return
    else
        error("Unknown command: " .. line .. "\n")
    end
end

-- a virtual line is either a physical line or
-- a multiline command
local function read_virtual_line(first_line_no)
    if first_line_no > #lines then return end
    local line = lines[first_line_no]
    local equal_signs = line:match("^%s*--%[(=*)%[%s*luatangle:")
    if not equal_signs then
        return line, first_line_no+1, first_line_no
    end
    local closing = ']' .. equal_signs .. ']'
    local physical_lines = {}
    for line_no = first_line_no,#lines do
        line = lines[line_no]
        local closing_start, closing_end = line:find(closing)
        if closing_end then
            if closing_end ~= #line then
                error("Command closing has trailing characters, line #" .. line_no .. "\n"
                   .. '  line is "' .. lines[line_no] .. '"\n'
                )
            end
            physical_lines[#physical_lines+1] = line:sub(1, closing_start-1)
            local first_line = physical_lines[1]:gsub('%[=*%[', '', 1)
            physical_lines[1] = first_line
            local full_line = table.concat(physical_lines, ' ')
            return full_line, line_no+1, first_line_no
        end
        physical_lines[#physical_lines+1] = line
    end
    error("Command starting at line number " .. first_line_no .. " never ends\n"
       .. '  line is "' .. lines[first_line_no] .. '"\n'
    )
end

while true do
    local line, command_line_number
    line, line_number, command_line_number = read_virtual_line(line_number)
    -- print("looking at line", line)
    if line == nil then break end
    if current_subsection then
        -- print("current subsection", current_subsection)
        local line_prefix = line:sub(1,#current_prefix)
        if line_prefix == current_prefix
        then
            command_do(line, command_line_number)
        elseif line_prefix:match("^[^%s]") then
            -- outdent ending section must not be a command
            current_subsection.last = line_number - 2
            section_end()
        elseif #line ~= 0 then
            local subsection_name = current_subsection.name
            local subsection_name_desc
                = subsection_name and '"' .. subsection_name .. '"' or '?'
            error("Section " .. subsection_name_desc .. " ends in bad line\n"
                .. " Problem line: " .. '#' .. line_number .. '"' .. line .. '"\n'
            )
        end
    else
        -- print("no current subsection")
        command_do(line, command_line_number)
    end
    -- A command may have ended or started a subsection
    if current_subsection then
        current_subsection.last = line_number - 1
    end
end

local function write_section(handle, section_name, parent_prefix, seen)
    if seen[section_name] then
        error("Section '" .. section_name .. "' is called recursively\n"
            .. "  That would create an infinite loop and is not allowed\n"
        )
    end
    local section = sections[section_name]
    if not section then
        error("Section '" .. section_name .. "' is used, but it was never defined\n")
    end
    section.used = true
    for subsection_ix = 1,#section do
        local subsection = section[subsection_ix]
        local subsection_prefix = subsection.prefix
        local line_no = subsection.first
        local last_line_of_subsection = subsection.last
        while line_no <= last_line_of_subsection do
            local virtual_line, first_command_line_no
            virtual_line, line_no, first_command_line_no = read_virtual_line(line_no)
            for command_line_no = first_command_line_no, line_no-1 do
                local command_line = lines[command_line_no]:sub(#subsection_prefix+1)
                handle:write(parent_prefix .. command_line, '\n')
            end
            virtual_line = virtual_line:sub(#subsection_prefix+1)
            local insert_prefix,raw_section_name = virtual_line:match("^(%s*)--%s*luatangle:%s+insert%s+(.*)$")
            if insert_prefix then
                local words = {}
                for word in raw_section_name:gmatch("[^%s]+") do
                    words[#words+1] = word
                end
                local insert_section_name = form_section_name(line, words, 1)
                seen[section_name] = true
                write_section(
                    handle, insert_section_name,
                    parent_prefix .. insert_prefix,
                    seen)
                seen[section_name] = nil
            end
        end
    end
end

local was_output
for section_name,output_filename in pairs(outputs) do
    was_output = true
    local handle
    if output_filename == '-' then
        handle = io.output()
    else
        local error_message
        handle,error_message = io.open(output_filename, 'w')
        if not handle then error(error_message) end
    end
    write_section(handle, section_name, '', {})
end

if not was_output then
    error("No output commands: there must be at least one\n")
end

for section_name,section in pairs(sections) do
    if not section.used then
         print("Section not used: ", section_name)
    end
end

-- vim: expandtab shiftwidth=4: