#! /usr/bin/env ruby
#
# NOTE: The working directory should be the main capp directory when this script is run
#
# $1 Cappuccino documentation directory


Encoding.default_external = 'UTF-8'


ACCESSOR_GET_TEMPLATE = <<EOS
/*!
Synthesized accessor method.
*/
- (\#{ivarType})\#{getter}
{
    return \#{ivar};
}

EOS

ACCESSOR_SET_TEMPLATE = <<EOS
/*!
Synthesized accessor method.
*/
- (void)\#{setter}:(\#{ivarType})aValue
{
    \#{ivar} = aValue;
}

EOS

ACCESSORS_IMPLEMENTATION_TEMPLATE = <<EOS

@implementation \#{className} (CPSynthesizedAccessors)

\#{accessorsSource}@end
EOS

DUMMY_IVAR = "    id __doxygen__;"


def makeHeaderFileFrom(fileName)
    # Grab the entire file (text)
    sourceFile = File.new(fileName, "r", :encoding => 'UTF-8')
    source = sourceFile.read
    sourceFile.close()

    # Add a @ before @implementation within comments so they are not considered as code
    source.gsub!(/(\/\*.*?\*\/)/m)  {|text| $1.gsub(/^(\s*)@implementation /, "\\1@@implementation ")}

    # If an implementation does not have an ivar block or an empty ivar block,
    # add one with a dummy ivar so that doxygen will parse the file correctly.
    source.gsub!(/^\s*(@implementation \s*\w+(?:\s*:\s*\w+)?)\n(\s*[^{])/, "\\1\n{\n#{DUMMY_IVAR}\n}\n\\2")
    source.gsub!(/^\s*(@implementation \s*\w+(?:\s*:\s*\w+)?)\n\s*\{\s*\}/, "\\1\n{\n#{DUMMY_IVAR}\n}")

    sourceFile = File.new(fileName, "w", :encoding => 'UTF-8')

    # Remove @accessor declarations from ivars before writing the source file
    sourceFile.write(source.gsub(/(\s*\w+\s+\w+)\s+@accessors(\(.+?\))?;/m, "\\1;"))

    # Extract all the @implementations blocks. Note, there may be more than one in a given .j file.
    m = source.scan(/^\s*(@implementation\s*(\w+)\s*(?::\s*\w+)?)\s*(?:\{(.*?)\})?(.*?)^\s*@end\s*$/m)

    return if m.length == 0

    for i in 0...m.length
        groups = m[i]
        declaration = groups[0]
        className = groups[1]
        ivars = groups[2]

        # Change "implementation" to "interface", create the .h file, and write the interface
        interfaceDeclaration = declaration.sub("@implementation", "@interface")
        interfaceFileName = File.dirname(fileName) + "/" + className + ".h"
        interfaceFile = File.new(interfaceFileName, "a")

        # Everything after this is ivar processing
        next unless ivars

        # Change @accessors declarations to a comment, doxygen chokes on them
        strippedIvars = ivars.gsub("@accessors", "// @accessors")
        interfaceFile.write("\n#{interfaceDeclaration}\n{#{strippedIvars}}\n@end\n")

        # Skip @accessors if it's a private class
        next if className[0, 1] == "_"

        writeAccessors(className, ivars, sourceFile, interfaceFile)
    end

    sourceFile.close()

    sourceFile = File.new(fileName, "r")
    source = sourceFile.read
    sourceFile.close()

    # Restore @implementation within comments
    sourceFile = File.new(fileName, "w")
    source.gsub!(/(\/\*.*?\*\/)/m)  {|text| $1.gsub(/^(\s*)@@implementation /, "\\1@implementation ")}
    sourceFile.write(source)
    sourceFile.close()
end

def writeAccessors(className, ivars, sourceFile, interfaceFile)
    # See if there are any @accessors in the ivars
    accessorsMatches = ivars.scan(/\s*(\w+)\s+(\w+)\s+@accessors(\(.+?\))?;/m)
    return if accessorsMatches.length == 0

    accessorsSource = ""

    # Create a CPSynthesizedAccessor category for the class with synthesized
    # accessor methods for each @accessors declaration.
    for accessorIndex in 0...accessorsMatches.length
        ivarDeclaration = accessorsMatches[accessorIndex]
        attributes = ivarDeclaration[2]
        next if attributes.nil?
        ivarType = ivarDeclaration[0];
        ivar = ivarDeclaration[1];

        attributesMatch = attributes.scan(/(\bproperty\s*=\s*(\w+)|\b(readonly)\b|\bgetter\s*=\s*(\w+)|\bsetter\s*=\s*(\w+))/m)
        next if attributesMatch.length == 0

        getter = nil
        setter = nil
        readonly = false

        for attributeIndex in 0...attributesMatch.length
            if not attributesMatch[attributeIndex][1].nil?   # property
                getter = attributesMatch[attributeIndex][1]
                setter = readonly ? nil : "set#{getter[0,1].upcase}#{getter[1..-1]}"
            elsif not attributesMatch[attributeIndex][2].nil?   # readonly
                readonly = true
                setter = nil
            elsif not attributesMatch[attributeIndex][3].nil?   # getter
                getter = attributesMatch[attributeIndex][3]
            elsif not attributesMatch[attributeIndex][4].nil? and not readonly   # setter
                setter = attributesMatch[attributeIndex][4]
            end
        end

        # Check for @accessors with no attributes
        if getter.nil? and setter.nil?
            getter = ivar
            setter = "set#{getter[0,1].upcase}#{getter[1..-1]}"
        end

        accessorsSource += makeAccessors(getter, setter, ivar, ivarType)
    end

    sourceFile.write(eval('"' + ACCESSORS_IMPLEMENTATION_TEMPLATE + '"'))
end

def makeAccessors(getter, setter, ivar, ivarType)
    accessors = ""

    if not getter.nil?
        accessors += eval('"' + ACCESSOR_GET_TEMPLATE + '"')
    end

    if not setter.nil?
        accessors += eval('"' + ACCESSOR_SET_TEMPLATE + '"')
    end

    return accessors
end

print "\033[36mGenerating header files...\033[0m\n"
fileList = Dir['AppKit.doc/**/*.j'] + Dir['Foundation.doc/**/*.j']

for fileName in fileList
    makeHeaderFileFrom(fileName)
end
