%%
%%  bdf2wingsfont.erl --
%%
%%     Conversion of BDF fonts to Wings' own font format.
%%
%%  Copyright (c) 2005-2009 Bjorn Gustavsson
%%
%%  See the file "license.terms" for information on usage and redistribution
%%  of this file, and for a DISCLAIMER OF ALL WARRANTIES.
%%
%%     $Id$
%%

-module(bdf2wingsfont).
-export([convert/1]).

-import(lists, [reverse/1,sort/1,foldl/3]).

-record(glyph,
	{code,					%Unicode for glyph.
	 bbx,					%Bounding box.
	 dwidth,				%Width.
	 bitmap}).

convert([Out|SrcFonts]) ->
    G = read_fonts(SrcFonts, []),
    io:format("  Writing ~s (~p glyphs)\n", [Out,length(G)]),
    write_font(G, Out),
    init:stop().

read_fonts([N|Ns], Acc) ->
    io:format("Reading ~s\n", [N]),
    {ok,F} = file:open(N, [binary,read,read_ahead,raw]),
    G = read_font(F),
    file:close(F),
    read_fonts(Ns, G++Acc);
read_fonts([], Acc) ->
    sort(Acc).

read_font(F) ->
    case read_line(F) of
	["STARTFONT","2.1"] ->
	    Ps = read_props(F),
	    G = read_font_glyphs(F),
	    to_unicode(G, Ps);
	Other ->
	    io:format("~p\n", [Other]),
	    error(invalid_bdf_file)
    end.

read_props(F) ->
    case read_line(F) of
	["STARTPROPERTIES",N0] ->
	    N = list_to_integer(N0),
	    read_props_1(F, N, []);
	_ ->
	    read_props(F)
    end.

read_props_1(_, 0, Acc) -> Acc;
read_props_1(F, N, Acc) ->
    P = read_one_prop(F),
    read_props_1(F, N-1, [P|Acc]).

read_one_prop(F) ->
    read_one_prop_1(raw_read_line(F), []).

read_one_prop_1(<<C,Cs/bytes>>, Key) when C =< $\s ->
    read_one_prop_2(Cs, reverse(Key));
read_one_prop_1(<<C,Cs/bytes>>, Key) ->
    read_one_prop_1(Cs, [C|Key]).

read_one_prop_2(<<C,Cs/bytes>>, Key) when C =< $\s ->
    read_one_prop_2(Cs, Key);
read_one_prop_2(Cs, Key) ->
    Val0 = reverse(skip_whitespace(reverse(binary_to_list(Cs)))),
    Val = convert_val(Val0),
    {Key,Val}.

convert_val("\""++Str0) ->
    "\""++Str = reverse(Str0),
    reverse(Str);
convert_val(Str) ->
    list_to_integer(Str).

read_font_glyphs(F) ->
    case read_line(F) of
	["CHARS",N0] ->
	    N = list_to_integer(N0),
	    try
		Gl = read_font_glyphs(F, N, []),
		case read_line(F) of
		    ["ENDFONT"] -> Gl;
		    ["STARTCHAR"|_] ->
			io:format("CHARS declaration said there were ~p glyphs;"
				  " but there is at least one more glyph.\n",
				  [N]),
			exit(error);
		    _Other ->
			io:format("Garbage instead of ENDFONT after last glyph\n"),
			exit(error)
		end
	    catch
		throw:{endfont,Left} ->
		    io:format("There are only ~p glyphs in this font; "
			      "CHARS declaration said there were ~p glyphs.\n",
			      [N-Left,N]),
		    exit(error)
	    end;
	_ ->
	    read_font_glyphs(F)
    end.

read_font_glyphs(_, 0, Acc) -> Acc;
read_font_glyphs(F, N, Acc) ->
    case read_line(F) of
	["STARTCHAR"|_] ->
	    G = read_one_glyph(F),
	    read_font_glyphs(F, N-1, [G|Acc]);
	["ENDFONT"] ->
	    throw({endfont,N})
    end.

read_one_glyph(F) ->
    read_one_glyph_1(F, #glyph{}).

read_one_glyph_1(F, G) ->
    case read_line(F) of
	["ENCODING",Code0] ->
	    Code = list_to_integer(Code0),
	    read_one_glyph_1(F, G#glyph{code=Code});
	["DWIDTH"|Ints] ->
	    Dwidth = [list_to_integer(S) || S <- Ints],
	    read_one_glyph_1(F, G#glyph{dwidth=Dwidth});
	["BBX"|Ints] ->
	    BBx = [list_to_integer(S) || S <- Ints],
	    read_one_glyph_1(F, G#glyph{bbx=BBx});
	["SWIDTH"|_] ->
	    read_one_glyph_1(F, G);
	["BITMAP"] ->
	    Bitmap = read_bitmap(F, []),
	    G#glyph{bitmap=Bitmap}
    end.
    
read_bitmap(F, Acc) ->
    case raw_read_line(F) of
	<<"ENDCHAR",_/bytes>> ->
	    list_to_binary(Acc);
	<<H1,H2,_/bytes>> ->
	    Hex = erlang:list_to_integer([H1,H2], 16),
	    read_bitmap(F, [Hex|Acc])
    end.

to_unicode(Gs, Ps) ->
    case proplists:get_value("CHARSET_REGISTRY", Ps) of
	"ISO10646" ->				%Already in Unicode.
	    filter_unicode(Gs);
	"ISO8859" ->
	    case proplists:get_value("CHARSET_ENCODING", Ps) of
		"1" -> Gs;
		"-"++Enc ->
		    to_unicode_1(Gs, "map-ISO8859-"++Enc);
		Enc ->
		    to_unicode_1(Gs, "map-ISO8859-"++Enc)
	    end
    end.

to_unicode_1(Gs0, MapName) ->
    Map = read_map(MapName),
    Gs = [G#glyph{code=gb_trees:get(C, Map)} || #glyph{code=C}=G <- Gs0],

    %% Throw away any Unicode characters falling in the 0x00 - 0xFF
    %% (ISO-8859-1) range. They are already defined in the ISO-8859-1 font.
    [G || #glyph{code=C}=G <- Gs, C >= 256].

filter_unicode(Gs) ->
    MapFiles = filelib:wildcard("map-ISO8859-*"),
    io:put_chars("  Filtering Unicode font to only include characters in:"),
    Map = foldl(fun(F, A) ->
			"map-"++CharSet = filename:basename(F),
			io:format(" ~s", [CharSet]),
			gb_trees:to_list(read_map(F)) ++ A
		end, [], MapFiles),
    io:nl(),
    Needed0 = [To || {_From,To} <- Map],
    Needed = gb_sets:from_list(Needed0),
    [G || #glyph{code=C}=G <- Gs, gb_sets:is_member(C, Needed)].
    

read_map(MapName) ->
    {ok,F} = file:open(MapName, [binary,read,read_ahead,raw]),
    Map = read_map_1(F, []),
    file:close(F),
    Map.

read_map_1(F, Acc) ->
    case read_map_line(F) of
	eof ->
	    gb_trees:from_orddict(orddict:from_list(Acc));
	["0x"++From0,"0x"++To0|_] ->
	    {ok,[From],[]} = io_lib:fread("~16u", From0),
	    {ok,[To],[]} = io_lib:fread("~16u", To0),
	    read_map_1(F, [{From,To}|Acc])
    end.

error(Term) ->
    throw({error,Term}).

read_map_line(F) ->
    case skip_whitespace(raw_read_line(F)) of
	eof -> eof;
	<<$#,_/bytes>> -> read_map_line(F);
	Cs -> collect_tokens(Cs)
    end.

read_line(F) ->
    case read_line_1(raw_read_line(F), F) of
	["COMMENT"|_] -> read_line(F);
	Line -> Line
    end.

read_line_1(eof, _) ->
    error(eof);
read_line_1(<<>>, Fd) ->
    %% Blank line - ignore and read the next line.
    read_line(Fd);
read_line_1(<<Ctrl,Line/bytes>>, Fd) when Ctrl =< $\s ->
    %% Ignore any leading whitespace (especially TAB and spaces).
    read_line_1(Line, Fd);
read_line_1(Line, _) ->
    collect_tokens(Line).

collect_tokens(Line) ->
    collect_tokens_1(Line, [], []).

collect_tokens_1(<<C,T/bytes>>, [], Tokens) when C =< $\s ->
    collect_tokens_1(T, [], Tokens);
collect_tokens_1(<<C,T/bytes>>, Curr, Tokens) when C =< $\s ->
    collect_tokens_1(T, [], [reverse(Curr)|Tokens]);
collect_tokens_1(<<C,T/bytes>>, Curr, Tokens) ->
    collect_tokens_1(T, [C|Curr], Tokens);
collect_tokens_1(<<>>, [], Tokens) ->
    reverse(Tokens);
collect_tokens_1(<<>>, Curr, Tokens) ->
    collect_tokens_1(<<>>, [], [reverse(Curr)|Tokens]).

skip_whitespace(<<C,Cs/bytes>>) when C =< $\s ->
    skip_whitespace(Cs);
skip_whitespace([C|Cs]) when C =< $\s ->
    skip_whitespace(Cs);
skip_whitespace(Cs) -> Cs.

raw_read_line(F) ->
    case file:read_line(F) of
	eof -> eof;
	{ok,Line} -> Line
    end.

%%%
%%% Writing the font.
%%%

write_font(G, Out) ->
    #glyph{bbx=[W,H,_,_]} = findchar(0, G),
    {Gl,Bit} = write_font_1(G, 0, [], []),
    Key = list_to_atom(filename:rootname(filename:basename(Out))),
    Desc = atom_to_list(Key),
    Font = {Key,Desc,W,H,Gl,Bit},
    Term = {wings_font,?wings_version,Font},
    Bin = term_to_binary(Term, [compressed]),
    file:write_file(Out, Bin).

write_font_1([#glyph{code=C,bbx=BBx,dwidth=Dwidth,bitmap=B}|Gs],
	     Offset, GlAcc, BiAcc) ->
    [W,H,Xorig,Yorig] = BBx,
    [Xmove,0] = Dwidth,
    G = {C,W,H,-Xorig,-Yorig,Xmove,Offset},
    write_font_1(Gs, Offset+size(B), [G|GlAcc], [B|BiAcc]);
write_font_1([], _, GlAcc, BiAcc) ->
    {reverse(GlAcc),list_to_binary(reverse(BiAcc))}.

findchar(C, [#glyph{code=C}=G|_]) -> G;
findchar(C, [_|Gs]) -> findchar(C, Gs).
