#! /usr/bin/env ruby
#----------------------------------------------------------------------
# chkfontpath.rb - utility for manipulating X Font Server font paths
#
# Copyright(C) 2003 Momonga Project.
# Author: Kenta MURATA <muraken2@nifty.com>
#
# $Id: chkfontpath.rb,v 1.3 2003/11/14 12:46:33 zunda Exp $
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Library General Public License for more details.
#
# You should have received a copy of the GNU Library General Public
# License along with this library; if not, write to the
# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
# Boston, MA 02111-1307, USA.
#
# [changelog]
# * Mon Aug  2 2004 zunda <zunda at freeshell.org>
# - FontPath::normalize(): validate unix/:7100, thanks to mute
# * Tue Nov 18 2003 zunda <zunda at freeshell.org>
# - Totally renovated: new classes: FontPath, FontPaths,
#   ConfigFileFactory, XFSConfig, and XF86Config.
# - can handle a situation where one of the three files is missing.
# - makes backup files with extension .chkfontpath
# * Thu Nov 14 2003 zunda <zunda at freeshell.org>
# - ChkFontPath.error?: hopefully better error handling
# * Thu Nov 13 2003 zunda <zunda at freeshell.org>
# - ChkFontPath::normalized_path, ChkFontPath::notail_path: added for
#   normalization of font paths: i.e. with / at the end
# - read_font_path: simplified paths parsing
# - include_path_x?: changed to use notail_path
# - include_path?: changed to use include_path_x?
# * Thu Nov 13 2003 Kenta MURATA <muraken2@nifty.com>
# - First release.
#
#----------------------------------------------------------------------

require "optparse"
require "singleton"

# a path for a font
class FontPath
	include Enumerable
	attr_reader :original	# as specified in the file or in the command line
	attr_reader :normalized	#	with / at the end but before :unscale if there is
	attr_reader :shrank	# without :unscale or whatever

	# specify ancestor if a FontPath is defined automatically
	def initialize( path, ancestor = nil )
		if ancestor then
			@original = ancestor
		else
			@original = path
		end
		@normalized = normalize( path )
		@shrank = valid? ? @normalized.sub( /:\w+$/, '' ) : ''
	end

	# true if the path is valid in a sence of man XF86Config or man xfs
	def valid?
		@normalized ? true : false
	end

	# true if the path really exists and readable
	def fonts_dir
		"#{@shrank}fonts.dir"
	end

	def exist?
		begin
			File.readable?( fonts_dir )
		rescue SystemCallError
			false
		end
	end

	# a String
	def to_s
		@normalized
	end

	# comaprison
	def ==(other)
		if FontPath == other.class then
			self.normalized == other.normalized
		else
			raise TypeError, "Comparison with a #{other.class}"
		end
	end

	# types returns [first_type, second_type]
	# zunda doesn't know what this is, just took from the original source.
	def types
		case File.basename( @normalized )
		when 'misc'
			[0, 3]
		when '75dpi'
			[1, 5]
		when '100dpi'
			[2, 6]
		else
			[100, 4]
		end
	end

	def need_unscaled?
		types[0] < 10
	end

	# type
	# zunda doesn't know what this is, just took from the original source.
	def type
		case File.basename( @normalized.sub( %r!/:!, ':' ) )
		when 'misc:unscaled'
			0
		when '75dpi:unscaled'
			1
		when '100dpi:unscaled'
			2
		when 'misc'
			3
		when '75dpi'
			5
		when '100dpi'
			6
		else
			4
		end
	end

	# path to the font (really?)
	def dirname
		File.dirname( @normalized )
	end

	# FontPath of `:unscaled' font
	def unscaled
		FontPath::new( "#{@normalized}:unscaled", @original )
	end

	# returns path String or nil for invalid path
	def normalize( path )
		npath = path.gsub( /\/+/, '/' )
    # man XF86Config: <absolute path>/ or <absolute path>/:unscaled
    if %r!\A(/.*?)/?(:\w+)?\z! =~ npath then
      return "#{$1}/#{$2}"
    end
    # man xfs: tcp/hostname:port or tcp/hostname:port/cataloguelist
    return npath if %r!\Atcp/[^:]+:\d+(/.+)?\z! =~ npath
    # man xfs: decnet/nodename::font$objname or decnet/nodename::font$objname/cataloguelist
    return npath if %r!\Adecnet/[^:]+::.+\$.+(/.+)?\z! =~ npath
    # man XF86Config: <trans>/<hostname>:<port-number>
    # a unix domain does not have a hostname
    return npath if %r!\A[^/]+/[^:]*:\d+?\z! =~ npath
    return nil
	end
	private :normalize

end	# class FontPath

# a FontPaths is an array of font paths
class FontPaths < Array

	# adds a FontPath
	def add( fontpath, force_first = false )
		first_type, second_type = fontpath.types

    i, last, first, second = [0, nil, nil, nil]
    while i < self.length do
      begin
        next if self[i].dirname != fontpath.dirname
        last = i
				type = self[i].type
        first = i if first.nil? and first_type < type
        second = i if second.nil? and second_type < type
      ensure
        i += 1
      end
    end

    last = self.length if last.nil?
    first = last if first.nil? and first_type < 10
    second = last if second.nil?

    if force_first then
      first = 0 if first_type < 10
      second = 0
    end
    self[first, 0] = fontpath.unscaled if first_type < 10
    self[second, 0] = fontpath

	end

end	# class FontPaths

# a ConfigFile is information in a configuration file
class ConfigFileFactory
	attr_reader :path
	attr_reader :fontpaths
	attr_reader :errors	# errors occured handling the file and font paths

	# new instance with the type
	def self::with_type( path, type )
		eval( "#{type}::new( #{path.dump} )" )
	end

	# specify the path for the config file
	def initialize( path )
		@path = path
		@fontpaths = FontPaths::new
		@errors = []
		@lines = []
		read	# to be implemented in sub classes
	end

	# should return a String of the content of the file
	def to_s
		raise StandardError, 'Not implemented'
	end

	# should read lines into @lines from the file
	def read
		raise StandardError, 'Not implemented'
	end
	private :read

	# overwrite the config file
	# set backup to nil if you dont want backup files
	def write!( backup = '.chkfontpath' )
		begin
			st = File.stat( @path )
			File.rename( @path, @path + backup ) if backup
			File.open( @path, 'w' ) do |f|
				f.print to_s
			end
			File.chmod( st.mode, @path )
		rescue SystemCallError
			@errors << $!
		end
	end

	# true if an error occured during processing the config file
	def error?
		@errors.size > 0
	end

	# true if font paths listed in the file contains the fontpath
	def include?( fontpath )
		@fontpaths.include?( fontpath )
	end

	# checks the validity of fontpath and adds it
	# returns true if successuful
	def add( fontpath, force_first = false )
		fontpath = FontPath::new( fontpath ) unless FontPath == fontpath.class
		unless fontpath.valid? then
			@errors << "Font directories must be absolute. Not adding #{fontpath.original} to #{@path}"
			return false
		end
		unless fontpath.exist? then
			@errors << "Error opening #{fontpath.fonts_dir}. Not adding the path to #{@path}"
			return false
		end
		if include?( fontpath ) then
			@errors << "#{fontpath.original} already listed. Not adding the path to #{@path}"
			return false
		end
		@fontpaths.add( fontpath, force_first )
		return true
	end

	# checks if the path really in the list, and remove it
	# returns true if successuful
	def remove( fontpath )
		fontpath = FontPath::new( fontpath ) unless FontPath == fontpath.class
		unless @fontpaths.reject!{ |path| fontpath.shrank == path.shrank } then
			@errors << "#{fontpath.original} not found in #{@path}"
			return false
		end
		return true
	end

	# list of the paths
	def path_list
		if @fontpaths.empty? then
			r = "No font path in #{@path}"
		else
			r = "Current font paths in #{@path}\n"
			@fontpaths.each_with_index do |path, i|
				r << "#{'%2d' % (i + 1)}: #{path.to_s}\n"
			end
		end
		r
	end

end	# class ConfigFileFactory

# fs/config
class XFSConfig < ConfigFileFactory

	def read
		begin
			catalogue = nil
			in_catalogue = false
			detected_catalogue = false
			IO.foreach( @path ) do |line|
				unless in_catalogue then
					if not detected_catalogue and /^\s*catalogue\s*=\s*/ =~ line then
						# font paths starting
						catalogue = $'
						in_catalogue = true
						detected_catalogue = true
						@lines << @fontpaths
					else	# normal line
						@lines << line
					end
				else	# scanning through the catalogue lines
					if /^\S/ =~ line then	# not a font path
						in_catalogue = false
						@lines << line
					else	# a font path
						catalogue << line
					end
					unless /,$/ =~ line then	# no more font paths
						in_catalogue = false
					end
				end
			end	# IO.foreach
			if catalogue then
        catalogue.strip.split( /[,\s]+/ ).each do |path| 
					fontpath = FontPath::new( path )
					if fontpath.valid? then
						@fontpaths << fontpath
					else
						@errors << "Invalid font path: #{fontpath.original}"
					end
        end
			end
		rescue SystemCallError
			@errors << "Error in reading #{@path}"
		end
	end

	# returns the source with maybe updated font paths
	def to_s
		r = ''
		@lines.each do |line|
			if String == line.class then
				r << line
			elsif FontPaths == line.class then
				if @fontpaths.size > 0 then
					r << "catalogue = "
					r << @fontpaths.collect{ |fontpath| fontpath.normalized }.join( ",\n\t" )
					r << "\n"
				end
			else
				raise TypeError, "line is a #{line.class.to_s}"
			end
		end
		r
	end

end	# class XFSConfig

# XF86Config or XF86Config-4
class XF86Config < ConfigFileFactory

	def read
		begin
			in_file_section = false
			font_path_detected = false
			IO.foreach( @path ) do |line|
				unless in_file_section then
					if /^\s*Section\s+"Files"/ =~ line then
						in_file_section = true
					end
					@lines << line	# normal line out of Files section
				else
					if /^\s*FontPath\s+"([^"]+)"/ =~ line then	# a font path
						@lines << @fontpaths unless font_path_detected
						font_path_detected = true
						fontpath = FontPath::new( $1 )
						if fontpath.valid? then
							@fontpaths << fontpath
						else
							@errors << "Invalid font path: #{fontpath.original}"
						end
					else
						if /^\s*EndSection\b/ =~ line then
							file_section = false
						end
						@lines << line
					end
				end	# unless in_file_section
			end	# IO.foreach
		rescue SystemCallError
			@errors << "Error in reading #{@path}"
		end
	end

	# returns the source with maybe updated font paths
	def to_s
		r = ''
		@lines.each do |line|
			if String == line.class then
				r << line
			elsif FontPaths == line.class then
				@fontpaths.each do |fontpath|
					r << %Q[\tFontPath "#{fontpath.normalized}"\n]
				end
			else
				raise TypeError, "line is a #{line.class.to_s}"
			end
		end
		r
	end

end	# XF86Config

class ChkFontPath

	CONFIG_FILES = {
		'/etc/X11/fs/config' => :XFSConfig,
		'/etc/X11/xorg.conf' => :XF86Config,
#		'/etc/X11/XF86Config' => :XF86Config,
#		'/etc/X11/XF86Config-4' => :XF86Config,
	}

  class CommandLine
    include Singleton

    attr_reader :argv

    attr_reader :add_dir

    attr_reader :del_dir

    attr_reader :list
    alias :list? :list

    attr_reader :quiet
    alias :quiet? :quiet

    attr_reader :first
    alias :first? :first

    def initialize
			@add_dir = nil
			@del_dir = nil
			@list = false
			@quiet = false
			@first = false
      ARGV.options do |q|
        q.on("-a", "--add=DIRECTORY",
             String, "add DIRECTORY to font path") do |dir|
          if not File.directory?(dir) then
          end
          @add_dir = dir
        end
        q.on("-r", "--remove=DIRECTORY",
             String, "remove DIRECTORY from font path") do |dir|
          if not File.directory?(dir) then
          end
          @del_dir = dir
        end
        q.on("-l", "--list", "list all directories in font path") do
          @list = true
        end
        q.on("-q", "--quiet", "quiet operation; don't print anything to the screen") do
          @quiet = true
        end
        q.on("-f", "--first", "`--add' puts the directory first(not last) in the path") do
          @first = true
        end

        q.on("-h", "--help", "show this help") do
          puts q
          exit
        end

        q.parse!
      end

      ARGV.options = nil
    end
  end

  def restart_xfs
    if File.exist?("/proc/version") then
      # It's there, we can do the pidof w/o fear of mounting /proc.
      pid = `/sbin/pidof xfs`.strip
      system("kill -USR1 #{pid} >/dev/null 2>&1")
    end
  end
  private :restart_xfs

  def main
    @cmdline = ChkFontPath::CommandLine.instance
		@errors = []

		config_files = CONFIG_FILES
		
		config_files.keys.sort.each do |path|
			begin
				config = ConfigFileFactory::with_type( path, config_files[path] )
				next if config.error?
      	config.add( @cmdline.add_dir, @cmdline.first? ) if @cmdline.add_dir
      	config.remove( @cmdline.del_dir ) if @cmdline.del_dir
				next if config.error?
				config.write! if @cmdline.add_dir or @cmdline.del_dir
			ensure
				puts config.path_list if @cmdline.list?
				@errors.concat( config.errors )
			end
		end

		restart_xfs if @cmdline.add_dir or @cmdline.del_dir

    if not @cmdline.quiet? and @errors.size > 0 then
			@errors.each do |e|
				$stderr.puts "#{$0}: #{e}"
			end
			exit 1
		else
			exit 0
    end

  end
end

ChkFontPath::new.main

### :nodoc:
### Local Variables:
### mode: ruby
### ruby-indent-level: 2
### indent-tabs-mode: nil
### End:
