#!/usr/bin/ruby
#
# Find unused resources in all the apps found recursively under the current directory
# Usage:
#   find_unused_resources.rb [-html]
#
# If -html is specified, the output will be HTML, otherwise it will be plain text
#
# Author: cbeust@google.com

require 'find'

debug = false

@@stringIdPattern = Regexp.new("name=\"([@_a-zA-Z0-9 ]*)\"")
@@layoutIdPattern = Regexp.new("android:id=\".*id/([_a-zA-Z0-9]*)\"")

@@stringXmlPatterns = [
  Regexp.new("@string/([_a-zA-Z0-9]*)"),
  Regexp.new("@array/([_a-zA-Z0-9]*)"),
]

@@javaIdPatterns = [
  Regexp.new("R.id.([_a-zA-Z0-9]+)"),
  Regexp.new("R.string.([_a-zA-Z0-9]+)"),
  Regexp.new("R.array.([_a-zA-Z0-9]+)"),
  Regexp.new("R.color.([_a-zA-Z0-9]+)"),
  Regexp.new("R.configVarying.([_a-zA-Z0-9]+)"),
  Regexp.new("R.dimen.([_a-zA-Z0-9]+)"),
]


@@appDir = "partner/google/apps/Gmail"

def findResDirectories(root)
  result = Array.new
  Find.find(root) do |path|
    if FileTest.directory?(path)
      if File.basename(path) == "res"
        result << path
      else
        next
      end
    end
  end
  result
end

class UnusedResources
  attr_accessor :appDir, :unusedLayoutIds, :unusedStringIds
end

class FilePosition
  attr_accessor :file, :lineNumber

  def initialize(f, ln)
    @file = f
    @lineNumber = ln
  end

  def to_s
    "#{file}:#{lineNumber}"
  end

  def <=>(other)
    if @file == other.file
      @lineNumber - other.lineNumber
    else
      @file <=> other.file
    end
  end
end


def findAllOccurrences(re, string)
  result = Array.new

  s = string
  matchData = re.match(s)
  while (matchData)
    result << matchData[1].to_s
    s = s[matchData.end(1) .. -1]
    matchData = re.match(s)
  end

  result
end

@@globalJavaIdUses = Hash.new

def recordJavaUses(glob)
  Dir.glob(glob).each { |filename|
    File.open(filename) { |file|
      file.each { |line|
	@@javaIdPatterns.each { |re|
          findAllOccurrences(re, line).each { |id|
            @@globalJavaIdUses[id] = FilePosition.new(filename, file.lineno)
	  }
        }
      }
    }
  }
end

def findUnusedResources(dir)
  javaIdUses = Hash.new
  layouts = Hash.new
  strings = Hash.new
  xmlIdUses = Hash.new

  Dir.glob("#{dir}/res/**/*.xml").each { |filename|
    if ! (filename =~ /attrs.xml$/)
      File.open(filename) { |file|
        file.each { |line|
          findAllOccurrences(@@stringIdPattern, line).each {|id|
            strings[id] = FilePosition.new(filename, file.lineno)
          }
          findAllOccurrences(@@layoutIdPattern, line).each {|id|
            layouts[id] = FilePosition.new(filename, file.lineno)
          }
          @@stringXmlPatterns.each { |re|
            findAllOccurrences(re, line).each {|id|
              xmlIdUses[id] = FilePosition.new(filename, file.lineno)
            }
          }
        }
      }
    end
  }
 
  Dir.glob("#{dir}/AndroidManifest.xml").each { |filename|
    File.open(filename) { |file|
      file.each { |line|
        @@stringXmlPatterns.each { |re|
          findAllOccurrences(re, line).each {|id|
            xmlIdUses[id] = FilePosition.new(filename, file.lineno)
          }
        }
      }
    }
  }

  recordJavaUses("#{dir}/src/**/*.java")

  @@globalJavaIdUses.each_pair { |id, file|
    layouts.delete(id)
    strings.delete(id)
  }

  javaIdUses.each_pair { |id, file|
    layouts.delete(id)
    strings.delete(id)
  }

  xmlIdUses.each_pair { |id, file|
    layouts.delete(id)
    strings.delete(id)
  }

  result = UnusedResources.new
  result.appDir = dir
  result.unusedLayoutIds = layouts
  result.unusedStringIds = strings

  result
end

def findApps(dir)
  result = Array.new
  Dir.glob("#{dir}/**/res").each { |filename|
    a = filename.split("/")
    result << a.slice(0, a.size-1).join("/")
  }
  result
end

def displayText(result)
  result.each { |unusedResources|
    puts "=== #{unusedResources.appDir}"

    puts "----- Unused layout ids"
    unusedResources.unusedLayoutIds.sort { |id, file| id[1] <=> file[1] }.each {|f|
      puts "    #{f[0]} #{f[1]}"
    }

 
    puts "----- Unused string ids"
    unusedResources.unusedStringIds.sort { |id, file| id[1] <=> file[1] }.each {|f|
      puts "    #{f[0]} #{f[1]}"
    }
 
  }
end

def displayHtmlUnused(unusedResourceIds, title)

  puts "<h3>#{title}</h3>"
  puts "<table border='1'>"
  unusedResourceIds.sort { |id, file| id[1] <=> file[1] }.each {|f|
    puts "<tr><td><b>#{f[0]}</b></td> <td>#{f[1]}</td></tr>"
  }
  puts "</table>"
end

def displayHtml(result)
  title = "Unused resources as of #{Time.now.localtime}"
  puts "<html><header><title>#{title}</title></header><body>"

  puts "<h1><p align=\"center\">#{title}</p></h1>"
  result.each { |unusedResources|
    puts "<h2>#{unusedResources.appDir}</h2>"
    displayHtmlUnused(unusedResources.unusedLayoutIds, "Unused layout ids")
    displayHtmlUnused(unusedResources.unusedStringIds, "Unused other ids")
  }
  puts "</body>"
end

result = Array.new

recordJavaUses("java/android/**/*.java")

if debug
  result << findUnusedResources("apps/Browser")
else 
  findApps(".").each { |appDir|
    result << findUnusedResources(appDir)
  }
end

if ARGV[0] == "-html"
  displayHtml result
else
  displayText result
end